DSL Router Monitor
October 2016

 

DSL/Broadband Router Monitor/Watchdog

 

This project started as a simple test for an ENC28J60 ethernet module and finished as a "well-over-the-top" broadband router monitor/watchdog: Earlier this year, my broadband router would disconnect from the internet every week or so and the only way to get it to reconnect was to turn its power off and on again. As I run a couple of servers from home (for a weather station and some video cameras), this was especially inconvenient when I was away from home. It's recently started doing it again so something was needed to power-cycle the router automatically when required.

This Arduino project is plugged into a spare ethernet socket on the router. It constantly monitors the internet connection by sending "ping" requests to a remote web server and waits for a response. After a pre-set number of failed responses - which would indicate the router has lost connection to the internet - the monitor tries a different remote server to confirm it's not just a server failure. If the test to the second server also fails, power to the router (which is looped through the project) is turned off and on again.

Once the unit is receiving responses to pings again, it sends a 'Notification' to an Android device - smart phone or tablet - using a third party service: Notify My Android. A free app installed on your Android device will receive up to five notifications a day.

It's a bad idea to switch off your router too frequently because the broadband system may interpret it as a poor phone line and will reduce your connection speed in an attempt to improve things. Therefore, our Arduino router-monitor is limited to a maximum of one power cycle per hour - even if the pre-set number of failed ping responses occurs again during this timed lockout period. During the timed lockout period, a red LED on the unit's front panel is 'ON'. The lockout period can be adjusted between 1.0 and 4.0 hours in 0.5 hour increments - see below.

The timed lockout can be cancelled by pressing the unit's RESET button.

In addition to sending a Notification to your Android device, the Notify My Android service also sends a short message back to the Arduino router-monitor. The Arduino software extracts the date and time from this message and displays it on the router-monitor's 16x2 LCD display. The date and time will remain on the display for your reference until you manually press a CLEAR button on the unit's front panel.

When the timed lockout period has expired, the red LED goes out and the unit is free to power-cycle the router again if required. A second Notification is sent to your Android device as confirmation that everthing is back up and running but the date and time on the LCD display will still show the date & time the router was power-cycled as this is considered to be more useful.

The time is shown in GMT. The display shows "gmt" when the first Notification is received and "GMT" (upper case) once the second Notification arrives.

The Android notifications can be turned off if they're not required but the date and time will then not be shown on the display.

Note that, because of limitations in the ATmega328, all communication with the Notify My Android service is in plain text.

The LCD Display

The relatively small 16x2 LCD display doesn't provide much space so the messages it displays are a bit cryptic. Some of the possible displays:

     

 

Ping requests are given a low priority on the internet so some failed responses are to be expected. The router-monitor won't trigger a power-cycle until a pre-set number of consecutive failures is reached. As a guide to setting this pre-set number, the figure top, right on the display (see above) shows the highest number ever recorded during normal operations. You should monitor this number with the router powered separately for at least several days. The 'Maximum Errors' number (E:) should then be set to at least twice this.

Initial Setup

Pressing the SET button on the unit's rear panel causes the display to cycle through the various setting options:

The first press displays the Maximum Errors screen. This is the number of allowed failed ping responses before the unit power-cycles the router and sends a message to the Notify My Android service. The two front panel buttons are used to adjust (up and down) the desired pre-set maximum error number. (Minimum is 10, maximum is 99).

The next press of the SET button displays the Notify My Android screen. Press either button on the front panel to toggle between Yes and No. When set to No, you won't receive any Notifications on your Android device and the date & time of a power-cycle won't be shown on the screen.

 

When the SET button is pressed again, the number of hours that the power-cycle is locked out for can be set from 1.0 hours to 4.0 hours in increments of 0.5 hours. The upper (LIGHT) button increases the value and the lower (CLEAR) button decreases the value.

 

The next press of the SET button is the Manual Lockout? screen. Pressing either front panel button toggles between Yes and No. When set to Yes, the ping times will continue to be displayed and the error count will increment but the router will not be power-cycled. When set to No the normal timed lockout will work as usual.

The next press of the SET button shows the Daylight Saving? screen. The Notify My Android server always returns the time of the Notification in GMT. This will be displayed on the second row of the LCD. You can set the option to add one hour (+1) to display 'clock time' during British Summer Time (BST).

The next press allows you to set the "primary server." This is the server that is pinged most of the time. When two consecutive DNS lookup failures occur, the router-monitor switches to the secondary server and tries to send ping requests to it. The monitor will switch back to the primary server if DNS lookup is restored or after 10 minutes or when the router power-cycles or when the router lockout timer expires.

The two servers are set in the software as www.google.com and bbc.co.uk - either can be set as the primary server. Other servers could be used if you prefer by changing them near the top of the Arduino 'sketch'.

Pressing the SET button again will return the LCD screen to the normal display. The values set will be saved in memory. When power is removed from the unit and restored, the values will be recalled from memory. Remember that, when the monitor is running normally, power to the router is looped through the monitor so removing the monitor power will also power down the router.

When the router-monitor is running normally (not showing a setup screen), the top button on the front panel toggles the LCD backlight on and off. The lower button clears the date and time of the last router power-cycle from the bottom line of the display - although the date & time of the last power-cycle can be re-called any time by pressing the button again.

Daylight Saving Options

The options for the Daylight Saving? setting are "No" (Always show time as GMT); "Yes" (Add 1 hour to the received time); and "Auto" (This should automatically add the hour during British Summer Time). The "Auto" icon has three different forms as shown on the left.

When set to "Auto", the monitor will not know if it's BST or GMT until it receives a date & time from the Notify My Android service. To request a test message from Notify My Android to update the Auto status, press the LIGHT button and the CLEAR button together.

(Remember that a maximum of 5 notifications per day are allowed with the free Android App. For a one-off charge of about 3, the limit can be increased to 800 notifications per hour).


Tech

Ping requests are sent to either google.com or bbc.co.uk. One is set as the primary server in the Setup menu. Ping requests are sent to the primary server at five-second intervals. A response must occur within four seconds otherwise the error counter increments. Each ping begins with a DNS lookup request and, when the internet is lost, these DNS lookups fail. The ethercard library has a built-in 30-second wait for a DNS response and, during this time, the router-monitor is unresponsive to button-presses. In practice this hasn't proven to be too much of a problem.

Once the DNS lookup fails, the error counter is incremented twice for each failed ping response (actually, the ping request isn't even sent) so, although each ping now takes approximately 35 seconds, fewer failed responses are needed to trigger the router's power-cycle. This allows the monitor to be resilient to a relatively high number of failed pings whilst automatically having the effect of reducing the number required once the DNS failures begin.

If the DNS lookup to the primary server fails twice in succession, the monitor switches to the secondary server. The monitor will switch back to the primary server if DNS lookup is restored or after 10 minutes or when the router power-cycles or when the router lockout timer expires. If the primary server still fails, the monitor will switch to the secondary server again. The power cycle will ony occur if both servers haven't responded when the Maximum Error count is reached.

When the DNS lookup fails, the display wil show the server name prefixed with an exclamation mark. For example www.google.com will show on the display as !google. The www. is optional for most modern servers. If it is used, the monitor will display it on the Setup screen but not during a DNS failure as some space is reserved on the display for the error count.

Notify My Android requests (and their DNS lookups) are to www.notifymyandroid.com. The yellow LED lights when a request is sent and is extinguished when a reply is received - usually within 1 second, although occasionally the reply takes three or four seconds, presumably depending on how busy the Notify My Android server is. If DNS lookup to www.notifymyandroid.com fails, there is no indication on the LCD screen but the yellow LED will remaim lit and debugging information is shown on the Arduino serial monitor (at 57,600 Bd).

The Circuit

The circuit is straightforward enough. The router's normal power supply is plugged into the unit and regulated to 5 volts with an OKI-78SR-5 1.5A switching regulator. Initially, I was using a standard LM7805 linear voltage regulator but wasn't happy about the slightly excessive temperature rise.

The router's supply also passes through the normally closed contacts on a 5 volt relay and back out to supply the router through a short patch cable. The relay is operated from an ATmega328 output through a BC547 NPN transistor. Any general purpose NPN transistor, such as a 2N2222, or even a MOSFET, such as a 2N7000, will do here. The 1N4002 diode across the relay coil is the usual spike suppressor when power to the relay's coil switches on and off.

The 5.1v zener diode isn't really required. It's purpose is to protect the ATmega328 output pin from high voltage in the very unlikely (virtually impossible) event the 12 volts from the relay contacts finds its way past the relay coil and the transistor. The specified Omron relay is good quality from an well-known manufacturer. I'd recommend using a reputable supplier to ensure you get a genuine part.

By using the normally closed relay contacts for the router's supply, the relay is normally de-energized. In addition to consuming less power, this makes it relatively fail-safe if the unit should fail and it allows the unit to be reset without disturbing power to the router. Using a relay instead of, say, a MOSFET provides physical isolation between the 12 volt router supply and the ATmega328 IO without using an opto-isolator. The specified Omron relay has contacts rated at 30 volts DC 10A. The switching voltage regulator is rated at up to 36 volts DC input.

The ENC28J60 Ethernet module is widely available on eBay. Some are only able to handle 3.2 volts. Note that I used a 5 volt version. The 3.2 volts version would need a 3.2v regulator in its Vcc supply but, as the IC's IO are 5v tolerant no level-shifting would be needed on the signal lines. The Module is connected to the ATmega328 SPI pins D11, D12 and D13 (MOSI, MISO and SCK respectively). Note that the pinout is different on the 3.2v version to what I've designed the PCB layout for with the 5v version.

The 16x2 LCD module uses a commonly available I2C serial backpack - again from an eBay supplier. The ATmega328 I2C pins SDA and SCL (pins 27 and 28) connect to the backpack.

Some I2C backpacks have a selectable address, via a jumper or solder pads but the usual default address seems to be 0x27.

There's a very useful website which gives information on the various I2C backpacks including a useful I2C Address Scanner sketch for the Arduino so you can check your I2C backpack in case you need to alter the address in the line: LiquidCrystal_I2C lcd(0x27, 16, 2); in the router-monitor sketch (see below).

ATmega328 IO pins D2 to D8 (D5 isn't used) connect to the three front panel LEDs, the two front panel push buttons and the rear push button. A second rear push button connects to the ATmega328 RESET pin.

For the three LEDs, I've used a red LED on D8 to indicate that the router is in its one-hour (cannot be power-cycled) lockout period; a green LED on D6 to indicate power to the router and a yellow LED on D7 to indicate a request has been sent to the Notify My Android service. The yellow LED should be extinguished almost immediately as a reply from Notify My Android comes back.

The ATmega328 is programmed, using the Arduino IDE, through its RESET, RXD and TXD pins using a USB-to-RS232 TTL module, again easily obtainable on eBay. Look for one on which the DTR pin is brought out on the header as this performs the Auto-reset when programming the ATmega328. (eBay links tend to be "here today, gone tomorrow" but this link to a suitable module was correct the last time I checked).

PCB Layout

Download main PCB artwork in actual size PDF format

 

Download switch panels PCB artwork in actual size PDF format

 

Main Components

ATmega328 with Arduino bootloaderHobbytronics, eBay suppliers
16 MHz crystaleBay, Hobbytronics, various
HanRun 5 volt ENC28J60 Ethernet ModuleDeal-Extreme, eBay suppliers. Check it's a 5 volt version
Omron G5LA-1 SPDT 5v relayFarnell, eBay, etc
16x2 LCD display and I2C serial interfaceAvailable separately or as a unit. eBay. I used a black on yellow "transflective" display because it can be read without the backlight being on.
OKI-78SR-5/1.5-W36-C 5v Switching regulatorRS-online, Farnell
2 off 12 way double row IDC connectors (for Ethernet module)Farnell (#221-5248), eBay
134x129x54mm EnclosureCPC
Power socket for power-in from the router's power brick.
RCA socket for power-out to the router.
My router uses a 2.1mm DC power plug/socket. RCA sockets & plugs are rated at 2A for the patch lead to the router

 

Power Considerations

The 5 volt Regulator

The circuit takes less than 180mA and an LM7805 regulator dissipates about 1.3 watts - even so, it got hotter than I had expected, even with a heatsink with a thermal resistance of about 12°C/W . Although my router's power supply only provided 12v DC, I wanted a bit more 'headroom' to cope with a higher-voltage power supply, if I ever change the router.

So, rather than redesigning the PCB for a larger heatsink, I swapped the LM7805 for a drop-in replacement switching regulator. The input and output capacitors, which were already on the PCB, are still within the maximum specified for the switching regulator so it really is a direct drop-in replacement.

The ENC28J60 Module

I fitted a small self-adhesive heatsink to the ENC28J60 IC because I'd read some reports that it runs quite warm if it's left running for long periods. Although I hadn't noticed it myself, I thought better safe than sorry. The 5v version of the ENC28J60 has a 3.2 volt regulator on the underside of the PCB so the module needs some air flow by packing it off the main PCB with spacers or a couple of 2mm nuts run onto each 2mm mounting screw.

The Power Sockets

The style (and size) of the DC power input socket is dictated by the plug on the router's power supply. My router (a ZyXel) uses a 2.1mm DC plug although many are 2.5mm (Thomson TG582, for example).

I wanted to use something different for the DC power output socket to avoid confusion. After some searching, it seems that using an RCA socket for power out (normally associated with audio) is fairly common in commercial equipment. Decent quality PCB-mounted sockets are rated at 2A which should be more than enough to power most routers provided the plug is wired with suitable cable (not screened audio cable). Although probably not the ideal choice, it's difficult to find a suitable alternative that would mount easily on the PCB.

 

Construction

The top photo shows the completed PCB using an LM7805 5v regulator with a heatsink rated at about 12°C/W. As I noted above, the temperature rise wouldn't have allowed for a router power supply with a higher voltage so I replaced the LM7805 & heatsink with a switching regulator - shown in close up in the second photo. It does, of course, run virtually stone cold.

The bottom photo shows the completed unit with the interconnections between the PCB and the front & rear panels and with the programming lead still in place. The PCB pin layout for the 12-way IDC connector to the ENC28J60 module is designed to match the module that I used. Other modules may use a different pin layout.

 

The Arduino 'Sketch'

A note about the code: The ATmega328 has plenty of program memory in which to store the sketch but is very short on "dynamic memory" in which to store variables, perform mathematics, temporarily store memory pointers for function call return addresses, and so on. As a result, this sketch isn't the "tidiest" from the programming point of view (blocks of code are written twice instead of assigning them to a function, for example) but this does make better use of the plentiful program memory to leave available as much of the precious dynamic memory as possible. The sketch also makes extensive use of PROGMEM to store constant data, freeing up about 250 bytes of dynamic memory.

The response from the Notify My Android service includes the date & time in GMT (UTC). For my own convenience, I added the facility to add one hour to the displayed time (either manually or automatically) so it displays Local Time during the daylight saving months (March to October in Europe). It's a bit experimental and hasn't been tested thoroughly. The sketch uses a lot of "string literals" (text that doesn't change), so it was a good opportunity to learn something about using PROGMEM and pointers to store this data instead of wasting valuable dynamic memory.

Arduino Libraries:

Ethercard Library
Liquid Crystal I2C Library

When you register an account with Notify My Android, you will be given a 48 character key.
Enter the key between the quotes ("-----") in the space highlighted in yellow in the code below.

// DSL Router Monitor (c) August 2016 vwlowen.co.uk
// Based on ethercard examples 'ping' and 'notify my android'
// Compiled using Arduino IDE version 1.6.11  (AVR Board version 1.6.13)

// Dynamic memory is very tight in the ATmega328 (Arduino Uno). Expect stability problems if the code is
// modified even slightly.

// https://github.com/jcw/ethercard
// https://github.com/fdebrabander/Arduino-LiquidCrystal-I2C-library

#include <EtherCard.h>
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>

LiquidCrystal_I2C lcd(0x27, 16, 2);                   //This LCD is at I2C address 0x27. Others may differ.

const char charMap[] [8] PROGMEM = {                  // Data for user-defined LCD characters
  {14, 17, 14, 4, 4, 28, 4, 28},                      // Key icon
  {10, 31, 21, 31, 31, 14, 10, 27},                   // Android icon
  {4, 4, 4, 4, 4, 21, 14, 4},                         // Lower case 'g' with a proper descender!
  {0, 0, 14, 18, 18, 14, 2, 14},                      // Underlined exclamation (Auto daylight saving - status unknown)
  {0, 4, 21, 14, 21, 4, 0, 31},                       // Underlined asterisk (Auto daylight saving - BST)
  {4, 4, 4, 4, 0, 4, 0, 31}                           // Underline blank. (Auto daylight saving - GMT)
};

const char apihost[] PROGMEM = "www.notifymyandroid.com";  // NotifyMyAndroid server 

const char server1[] PROGMEM = "www.google.com";           // Main server to ping. 
const char server2[] PROGMEM = "bbc.co.uk";                // Secondary server to check when DNS lookup on main server fails.

byte offset1 = 0;                                          // Used to omit 'www.' from LCD if present in server url.
byte offset2 = 0; 


/* Variables to adjust for *UK* daylight saving time. (BST)  */
/* Gets date/time from Notify My Android reply.  */
/* Only works for +1 hr UK. */

const char sMon0[] PROGMEM = "Jan";
const char sMon1[] PROGMEM = "Feb";
const char sMon2[] PROGMEM = "Mar";
const char sMon3[] PROGMEM = "Apr";
const char sMon4[] PROGMEM = "May";
const char sMon5[] PROGMEM = "Jun";
const char sMon6[] PROGMEM = "Jul";
const char sMon7[] PROGMEM = "Aug";
const char sMon8[] PROGMEM = "Sep";
const char sMon9[] PROGMEM = "Oct";
const char sMon10[] PROGMEM = "Nov";
const char sMon11[] PROGMEM = "Dec";

const char * const mons_table[] PROGMEM {sMon0, sMon1, sMon2, sMon3, sMon4, sMon5, sMon6, sMon7, sMon8, sMon9, sMon10, sMon11};


const char sDow0[] PROGMEM = "Sun";
const char sDow1[] PROGMEM = "Mon";
const char sDow2[] PROGMEM = "Tue";
const char sDow3[] PROGMEM = "Wed";
const char sDow4[] PROGMEM = "Thu";
const char sDow5[] PROGMEM = "Fri";
const char sDow6[] PROGMEM = "Sat";

const char * const dows_table[] PROGMEM = {sDow0, sDow1, sDow2, sDow3, sDow4, sDow5, sDow6};

const char message0[] PROGMEM = "Router power-cycled.";       // Notify My Android messages
const char message1[] PROGMEM = "Router unlocked.    ";
const char message2[] PROGMEM = "Test notification.  ";

const char * const message_table[] PROGMEM = {message0, message1, message2};


byte datetime[16];                     // Used to "copy" data that is printed on LCD 2nd row for recall later.

byte nDay;                             // Day of the month
byte nDow;                             // Day of the week
byte nMon;                             // Month

#define BST 1                          // Add 1 hour for British Summer Time (Other positive values may work).
boolean daylightSaving = false;        // Show time in GMT or BST?
byte ds = 0;                           // 0 or 1 or 2 = GMT or BST or Auto ?

// ethernet interface mac address, must be unique on the LAN
static byte mymac[] = { 0x74,0x69,0x69,0x2D,0x30,0x31 };

byte Ethernet::buffer[800];

unsigned long pingTimer;
unsigned long errTimer;
unsigned long lockoutTimer;

#define errTimeout  4000000            // wait this long (microseconds) before reporting no ping response
#define pingTimeout 5000000            // wait this long (microseconds) between pings.  Note this time increases
                                       // to about 35 seconds when the DNS lookup fails as the DNS has to time out.
                                       // The DNS timeout is hard-coded at 30000 ms in the ethercard library (dns.cpp)
#define routerDelay 15000              // wait this long (milliseconds) when power-cycling the router. (15 seconds)

byte mins = 60;                        // lockTimeout will be calculated as minutes * 60000 msec.
unsigned long lockTimeout = 3600000;   // wait this long (milliseconds) before allowing another router cycling.


#define serverTimeout 600000           // Swap back to primary server after this time.
unsigned long serverTimer;

boolean useServer1 = true;             // Default is to use server1 (google.com)
boolean serverSwapped = false;
byte dnsErrCount = 0;
                                       
byte errCount = 0;                     // Accumulating number of errors (no response to pings) before the next response to pings.
byte errHighCount = 0;                 // Make a note of the highest number of errors before a response. This
                                       // helps determine the number of errors before triggering the router
byte maxError = 20;                    // re-cycle (maxError) and Notify My Android (NMA).

byte nmaAction = 0;                    // Determines message to send to Notify My Android service. 
byte op = 0;                           // Determines currently displayed setup screen on LCD and action up/down buttons will perform.

/* Hardware defines */

#define ethCS 10                       // ethercard chip select pin.

#define lockLED 8                      // Red LED lights on first power re-cycle and stays on until lockout times out (lockTimeout).  
#define nmaLED 7                       // Yellow LED to indicate NMA has been called but no response received (yet).  
#define PowerLED 6                     // Separate normally ON output for green LED which 'mirrors' the power to the router.
#define powerRelay A3                  // Normally OFF output to relay. Relay uses a normally closed contact to power the router.

           
#define clearLCD 4                     // Clear 'Status Line' (2nd Row) on LCD which normally shows the time of the last power cycle.
#define lightLCD 3                     // Push button to toggle LCD backlight on/off
#define setMax 2                       // Set push button


/* Each boolean actually uses a byte so is a "bit" wasteful (!) but is easier to implement and understand! */

boolean led = true;                    // Is LCD backlight on or off?
boolean timedLockout = false;          // Is router power cycle locked out for an hour?
boolean dnsFail = false;               // Has DNS lookup failed?

boolean manualLockout = false;         // Is router locked 'ON'  so it won't power-cycle even after a timed lockout?
boolean isLocked = false;
boolean nma = true;                    // Notify My Android?
boolean cleared = true;                // LCD 2nd row (status line) is clear.
boolean dateLine = false;


Stash stash;                           // Class to build request to Notify My Android.
static byte session;
char buff[21];

// Routine to send a formatted request to NotifyMyAndroid. As noted in the ethercard example, this data has to be 
// sent over the internet as plain text because the ATmega328 is limited in what it can do due to memory constraints.

static void notifyMyAndroid(const byte msg) {
  
  strcpy_P(buff, (char*) pgm_read_word(&(message_table[msg])));     // Retrieve message from Program Memory into buffer.

  digitalWrite(nmaLED, HIGH);                                       // Signal that we've called Notify My Android.

    
  if (!ether.dnsLookup(apihost)) {                          
      Serial.println(F("DNS lookup failed for the apihost"));  
  }  
  ether.printIp("SRV: ", ether.hisip);                          
   
  byte sd = stash.create();                                         // Create ethernet stash class.

  stash.print(F("apikey="));                 
  stash.print(F("------------------------------------------------"));   // Enter your NMA key when you register an account
                                                                    
  stash.print(F("&application="));
  stash.print(F("Arduino Watchdog"));

  stash.print(F("&event="));
  stash.print(F("Router monitor"));

  stash.print(F("&description="));
  stash.print(buff);                                                // Message retrieved from Program Memory.
  
  stash.print(F("&priority="));
  stash.print(F("0"));                                              // Priority 1 would over-ride Android's Do Not Disturb.

  stash.save();
  int stash_size = stash.size();

  // Compose the http POST request, taking the headers below and appending
  // previously created stash in the sd holder.
  Stash::prepare(PSTR("POST /publicapi/notify HTTP/1.1" "\r\n"
                      "Host: $F" "\r\n"
                      "Content-Length: $D" "\r\n"
                      "Content-Type: application/x-www-form-urlencoded" "\r\n"
                      "\r\n"
                      "$H"),
                 apihost, stash_size, sd);

  pingTimer = micros();                                  // Reset ping and error timers so
  errTimer = micros();                                   // they won't timeout while receiving Notification                     

  // send the packet - this also releases all stash buffers once done
  // Save the session ID so we can watch for it in the main loop.
  session = ether.tcpSend();
}


/* ====== Function to update 2nd row on LCD ("Status Line") ============ */

void updateStatus() {                                    // Update the 2nd row on the LCD

   lcd.setCursor(0,1);
   for (byte i=0;i<16;i++)
     lcd.print(F(" "));
   if (nma && !cleared && dateLine) {                    // 2nd row is showing date/time
      if (timedLockout)
        digitalWrite(lockLED, HIGH);
      lcd.setCursor(0,1);
      for (byte i=0; i<13; i++)
        lcd.write(datetime[i]);
        
      if (daylightSaving) {
        addHour();
        timedLockout ? lcd.print(F("bst")) : lcd.print(F("BST"));
      } else {
        if (timedLockout) {
           lcd.write(3);
           lcd.setCursor(14,1);
           lcd.print(F("mt"));
        }else
           lcd.print(F("GMT"));
      }
      return;
    }                                                     

    if (manualLockout) {                                  // 2nd row is showing normal status line
      lcd.setCursor(9,1);                                 // If manual lockout is selected...
      lcd.write(0);
      lcd.print(F("LOCKED"));                             // display 'LOCKED'.
      digitalWrite(lockLED, HIGH);
      
    } else {                                              // Not manually locked so show normal icons.
      
      lcd.setCursor(0,1);                                 // Display max error and lock time.
      lcd.print(F("E:"));    
      lcd.print(maxError);                                // Max errors before trigger router power cycle
      
      if (nma && !manualLockout) {
        lcd.setCursor(7,1);  
        lcd.write(1);                                     // Print Android symbol if Notify My Android is set
      }

       lcd.setCursor(8,1);

       if (daylightSaving) {                              // daylight saving will be known after a NMA response.
          ds==1 ? lcd.print(F("*")):lcd.write(4);
       }  else {
      
          switch (ds) {                                   // If daylight saving state isn't known, show 
            case 0: lcd.print(F(" "));                    // setting instead (No, Yes, Auto).
                    break;
            case 1: lcd.print(F("*"));                    // Yes
                    break;
            case 2: lcd.write(5);                         // Auto (state unknown)
                    break;
          }         
       }

      if (timedLockout || manualLockout) {
        digitalWrite(lockLED, HIGH);
        lcd.setCursor(9,1);
        lcd.write(0);                                       // Print a key symbol
      } else {
        digitalWrite(lockLED, LOW);
      }   
      lcd.setCursor(12,1);
      if (mins < 100) lcd.print(F(" "));
      lcd.print(mins);
      lcd.print(F("m"));  
    }
}


/* ================  Function to add 1 hour for daylight saving. =============== */

void addHour() {                                          // This is a simple GMT +1 hr. Memory constraints
    char hr[3];                                           // prevent anything more versatile.
     hr[0] = datetime[7];
     hr[1] = datetime[8];
     hr[2] = '\0';
           
     byte h = atoi(hr) + BST;                             // Add one hour to GMT time
     if (h > 23) {                                        // If hour crosses to next day....
        h = 0;                                            // increment day number.

        byte dom = nDay +1;                               // nDay is day of month as sent by Notify My Android
        
                                                          // if new day of month is > 30 and month is Apr, Jun, Sep or Nov.
        if ((dom > 30) && ((nMon == 4) || (nMon == 6) || (nMon == 9) || (nMon == 11))){
          dom = 1;                                         // Set day of month to 1
          nMon ++;                                         // Increment month
           
        }  else
        if (dom > 31) {                                    // if new day of month is more than 31...
          dom = 1;                                         // day of month = 1
          nMon ++;                                         // next month
        }

        lcd.setCursor(0,1);
        if (dom < 10) lcd.print(F("0"));                   // Print new day of month
        lcd.print(dom);
     }
      
     lcd.setCursor(7,1);
     if (h < 10) lcd.print(F("0"));
     lcd.print(h);
     lcd.setCursor(13,1);  
}

/* ======== Function to print character array from Program Memory one character at a time ======== */

void printProgStr (const char * str, const char ch, byte offset, byte total) {
  char c;
  byte b = 0;
  if (!str) return;

  str = str + offset;                                                      // Skip 'www.' if offset > 0.
    
  while ((c = pgm_read_byte_near(str++)) && (c != ch) && (b < total)) {    // Stop at ch character or 'total' characters.
    c == 'g' ? lcd.write(3) : lcd.print(c);                                // If character is 'g' print custom g
    b++;                                                                   // with a proper descender!
  }
} 

/* Function to determine if server url begins with www .  and returns appropriate number of characters to skip */

byte isWww(const char * str) {
  byte result = 0;
  while (pgm_read_byte_near(str++)=='w') result++;
  result == 3 ? result = 4 : result = 0;                             // Also skip the dot after 'www'
  return result;
}

/* ========  Setup ====================================== */

void setup () {
  pinMode(PowerLED, OUTPUT);
  pinMode(lockLED, OUTPUT);              // Lock LED (Router is unable to power-cycle when this LED is lit).
  pinMode(powerRelay, OUTPUT);           // Power to router LED.
  pinMode(nmaLED, OUTPUT);               // Notify My Android has been notified LED.
  pinMode(ethCS, OUTPUT);

  pinMode(lightLCD, INPUT_PULLUP);       // Backlight on/off
  pinMode(clearLCD, INPUT_PULLUP);       // Clear date/time from LCD
  pinMode(setMax, INPUT_PULLUP);         // Set Max error push button
  
  digitalWrite(powerRelay, LOW);         // Power on router
  digitalWrite(PowerLED, HIGH);
 
  lcd.begin();

  byte bb[8];                            // Retrieve custom LCD characters from PROGMEM and create characters.
  for (byte i=0;i<6;i++) {
    for (byte j=0;j<8;j++) bb[j] = pgm_read_byte(&(charMap[i][j]));
    lcd.createChar(i, bb);
  }
  
  Serial.begin(57600);
  
  Serial.println(F("\n[pings]"));
  lcd.clear();
  
  lcd.backlight();
  
  useServer1 = constrain(EEPROM.read(5), 0, 1);      // Select which server to use as main server
  ds = constrain(EEPROM.read(4), 0, 2);              // Is daylight savings set to GMT, BST or Auto? 
  manualLockout = constrain(EEPROM.read(3), 0, 1);   // Is manual lockout in force? no/yes
  nma = constrain(EEPROM.read(2), 0, 1);             // Get notify my android  no/yes.
  mins = constrain(EEPROM.read(1), 60, 240);         // Get number of minutes router is locked out for.
  maxError = constrain(EEPROM.read(0), 10, 99);      // Get max errors that are allowed from EEPROM. 
  
  offset1 = isWww(server1);                          // Determine if server urls begin with 'www.'
  offset2 = isWww(server2);

  
  lockTimeout = mins * 60000;

  for (byte i=0;i<16;i++)
    datetime[i]= ' ';
    
  lcd.setCursor(0,0);
  lcd.print(F("Initializing.."));
  
  if (ether.begin(sizeof Ethernet::buffer, mymac, ethCS) == 0) {       // Initialize ethercard.
    Serial.println(F("Failed to access Ethernet controller"));
    lcd.print(F("Controller Error"));
  }
  
  if (!ether.dhcpSetup())   {                        // DHCP must be enabled in the router so it can assign
    lcd.setCursor(0, 0);
    lcd.print(F("DHCP Failed  "));                   // the ethercard an IP address and get the gateway and
    lcd.setCursor(0,1);
    lcd.print(F("Check Connection"));
    Serial.println(F("DHCP failed"));                // DNS server addresses.
  } 
  while(!ether.dhcpSetup());                         // Loop here until DHCP is working
  
  lcd.clear();
  updateStatus();

  ether.printIp("IP: ", ether.myip);
  ether.printIp("GW: ", ether.gwip);
  ether.printIp("DNS:", ether.dnsip);  

  pingTimer = micros();                              // set the ping timer running
   
}



void loop () {

/* ========  Check if we're on the secondary server and swap back to primary server after 10 minutes ======= */  

  if (serverSwapped) {
    if (millis() - serverTimer >= serverTimeout) {
       useServer1 = constrain(EEPROM.read(5), 0, 1);                        // Swap back to main server
       serverSwapped = false;
       dnsErrCount = 0;      
    }
  }


/* ========  Check if the router cycle lockout timer has expired ======================================== */

  if (timedLockout) {                                                        // If router power-cycle is locked out, check the
    if (cleared) {                                                           // elapsed time and - if the 2nd row on the LCD
      byte left = ((lockTimeout - (millis() - lockoutTimer)) / 60000) + 1;   // isn't showing the date/time, show the elapsed
      lcd.setCursor(11,1);                                                   // time.
      if (left < 100) lcd.print(F(" "));
      lcd.write(2);
      if (left < 10)  lcd.print(F("0"));
      lcd.print(left);
      lcd.print(F("m"));
    }
    if (millis() - lockoutTimer >= lockTimeout) {                            // If time has expired...
      digitalWrite(lockLED, LOW);                                            // 'Lock' LED OFF.
      timedLockout = false;                                                  // Release the timed lockout flag.
      if (serverSwapped) {
        useServer1 = constrain(EEPROM.read(5), 0, 1);                        // Swap back to main server
        serverSwapped = false;
        dnsErrCount = 0;
      }
      isLocked = false;
      updateStatus();                                                        // Update the 2nd row of the LCD
      if (nma && !manualLockout) {
        nmaAction = 2;                                                       // Tell Notify my android.
        notifyMyAndroid(1);                                                  // 'Router unlocked' message
      }
     
    }
  }


  /* ===================  Front Panel Switches ==================================================== */

  if (digitalRead(clearLCD) == LOW) {                     // If 'Clear' button is pressed, update
    delay(250);                                           // the LCD's 2nd row.
    if (digitalRead(lightLCD) != LOW) {                   // Check 'Light' button isn't pressed also.   
      cleared = !cleared;
      updateStatus();                        
    } 
  }
  

  if (digitalRead(lightLCD) == LOW) {                     // Toggle LCD backlight on/off
    delay(250);
    if (digitalRead(clearLCD) != LOW) {                   // Chec 'Clear' button isn't pressed also.
      led = !led;
      led ? lcd.backlight(): lcd.noBacklight();
    }
  }

  if ((digitalRead(clearLCD) == LOW) && (digitalRead(lightLCD) == LOW)) {     // Press both buttons for
    delay(250);                                                               // a test Notification
    while ((digitalRead(clearLCD) == LOW) || (digitalRead(lightLCD) == LOW));

    nmaAction = 1;                                        
    notifyMyAndroid(2);      
  }


  /* ===================  Main Settings menu ========================================================= */
  

  if (digitalRead(setMax) == LOW) {                       // If 'Set' button is pressed, enter main 'Setup' loop
      while (digitalRead(setMax) == LOW);                 // Wait here until button is released.
      op = 0;                                             // Reset 'operation' variable.
      lcd.clear();
      delay(150);
      lcd.print(F("Set Max Errors"));                     
      lcd.setCursor(0,1);
      lcd.print(maxError);
      while (op == 0) {
        while (digitalRead(lightLCD) == LOW) {            // Use LCD backlight on/off button to increase max error.
          if (maxError < 99) maxError += 1;
          lcd.setCursor(0,1);
          lcd.print(maxError);
          lcd.print(F(" "));
          delay(250);
        }
        while (digitalRead(clearLCD) == LOW) {            // Use  clear  button to decrease max error.
          if (maxError > 10) maxError -= 1;
          lcd.setCursor(0,1);
          lcd.print(maxError);
          lcd.print(F(" "));   
          delay(250);
        }
        if (digitalRead(setMax) == LOW) {                 // Cycle indefinitely until 'SetMAx' button is pressed again
          op = 1;
          while(digitalRead(setMax) == LOW);
        }
     }

    delay(50);
    lcd.clear();
    lcd.print(F("Notify MyAndroid"));                     // Enable/disable Notify My Android
    while (op == 1) {
      lcd.setCursor(0,1);                                 // Display current NMA status
      if (nma ) {
        lcd.print(F("Yes "));
        lcd.write(1);
      } else {
        lcd.print(F("No   "));
      }
      while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) {  // ======== Set Notify My Android ====
        nma = !nma;
        lcd.setCursor(0,1);
        if (nma ) {
           lcd.print(F("Yes "));
           lcd.write(1);
        } else {
           lcd.print(F("No   "));
        }       
        delay(250);
      }
      if (digitalRead(setMax) == LOW) {                   // Press 'SetMax' again to exit NMA loop
         op = 2;
         while(digitalRead(setMax) == LOW);
      }          
    }     
    
     delay(50);
     lcd.clear();                                         // Set the number of minutes (in 30 minute increments)
     lcd.print(F("Timed Lockout "));                      // that the route rpower-cycle can be locked out for.
     lcd.write(0);
     while (op == 2) {
       lcd.setCursor(0,1);
       lcd.print(mins);
       lcd.print(F(" min ("));
       lcd.print((float)mins/60,1);                       // Display minutes in hours. (1.0, 1.5, 2.0.... 4.0)
       lcd.print(F(" hr)  "));
       while (digitalRead(lightLCD) == LOW) {             // Use LCD backlight on/off button to increase minutes.
         if (mins < 240) mins += 30;                      // Increase minutes by 30 minute intervals.
         lcd.setCursor(0,1);
         lcd.print(mins);
         lcd.print(F(" min ("));
         lcd.print((float)mins/60,1);                     // Display minutes in hours. (1.0, 1.5, 2.0.... 4.0)
         lcd.print(F(" hr)  "));
         delay(250);
       }
       while (digitalRead(clearLCD) == LOW) {             // Use  clear error button to decrease minutes.
         if (mins > 60) mins -= 30;                       // Decrease minutes by 30 minute intervals.
         lcd.setCursor(0,1);
         lcd.print(mins);
         lcd.print(F(" min ("));
         lcd.print((float)mins/60,1);                     // Display minutes in hours. (1.0, 1.5, 2.0.... 4.0)
         lcd.print(F(" hr)  "));
         delay(250);
       }
       if (digitalRead(setMax) == LOW) {                  // Press 'SetMax' again to exit Set Lockout loop
         op = 3;
         while(digitalRead(setMax) == LOW);
       }
    } 


    delay(50);
    lcd.clear();
    
    lcd.print(F("Manual Lockout?"));                      // Set/unset Manual Lockout
    while (op == 3) {
      lcd.setCursor(0,1);
      if (manualLockout ) {                               // Display current Manual lockout status
        lcd.print(F("Yes  "));
      } else {
        lcd.print(F("No   "));
      }
      while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) {
        manualLockout = !manualLockout;
        lcd.setCursor(0,1);
        manualLockout ? lcd.print(F("Yes")):lcd.print(F("No "));    
        delay(250);
      }
      if (digitalRead(setMax) == LOW) {                   // Press 'SetMax' again to exit Manual Lockout loop
         op = 4;
         while(digitalRead(setMax) == LOW);
      }          
    } 

    delay(50);
    lcd.clear();
    
    lcd.print(F("Daylight Saving?"));                     // Set Daylight saving selector
    while(op == 4) {
      lcd.setCursor(0,1);
      switch (ds) {
        case 0: lcd.print(F("No  "));
                break;
        case 1: lcd.print(F("Yes "));
                break;
        case 2: lcd.print(F("Auto"));
                 break;
        }
        while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) {
          ds += 1;
          if (ds > 2) ds = 0;
          lcd.setCursor(0,1);
          switch (ds) {
            case 0: lcd.print(F("No  "));
                    break;
            case 1: lcd.print(F("Yes "));
                  break;
            case 2: lcd.print(F("Auto"));
                    break;
          }
          delay(250);
        }

        if (digitalRead(setMax) == LOW) {                                    // Press 'SetMax' again to exit Daylight Saving loop
           op = 5;
           while(digitalRead(setMax) == LOW);
      }  
    }
    delay(50);
    lcd.clear();
    
    lcd.print(F("Primary server"));                                          // Set the primary server
    while (op == 5) {
      lcd.setCursor(0,1);
      useServer1 ? printProgStr(server1, '!',0, 16) :                        // Print server name from program memory and
                   printProgStr(server2, '!', 0, 16);                        // include 'www' if present in url.

      while ((digitalRead(lightLCD) == LOW) || (digitalRead(clearLCD) == LOW)) {
        useServer1 = !useServer1;                                            // Swap servers as the primary server
        lcd.setCursor(0,1);
        lcd.print(F("               "));
        lcd.setCursor(0,1);
        useServer1 ? printProgStr(server1, '!', 0, 16) :                     // Print server name from program memory
                     printProgStr(server2, '!', 0, 16);                      // include 'www' if present in url.
        delay(250);
      }
      
      if (digitalRead(setMax) == LOW) {                                      // Press 'SetMax' again to exit settings loop
         op = 6;
         while(digitalRead(setMax) == LOW);
      }          
    }
      
    lcd.clear();
    
    if (EEPROM.read(5) != useServer1) EEPROM.write(5, useServer1);
    if (EEPROM.read(3) != manualLockout) EEPROM.write(3, manualLockout);
    if (EEPROM.read(2) != nma) EEPROM.write(2, nma);
    if (EEPROM.read(1) != mins) EEPROM.write(1, mins);
    if (EEPROM.read(0) != maxError) EEPROM.write(0, maxError);   // Save values in EEPROM but don't write unnecessarily.
    
    if (EEPROM.read(4) != ds) {
      EEPROM.write(4, ds); 
      if (dateLine) {                                            // If Daylight Saving selection has changed, and a
        while(digitalRead(setMax) == LOW);                       // date/time has been received previously, re-evaluate
        delay(100);                                              // the time.
        switch (ds) {                                            // Check setting of Daylight Saving selector
          case 0: daylightSaving = false;                        // No, Yes, Auto
                  break;
          case 1: daylightSaving = true;
                  break;
          case 2: daylightSaving = isSummer();
                  break;
          default: daylightSaving = false;
                   break;
        }
      }
    }
    
    updateStatus();                                              // Display (new) settings on 2nd row of LCD
   
    lockTimeout = mins * 60000;                                  // Convert minutes to milliseconds
  }

/* ===================================  End of Main Settings Menu ======================================= */



/* ping a remote server once every few seconds. I found it necessary to use DNS lookup every time  *
 * around the loop because the ethercard also looks up the address of notifymyandroid.com          */
 
  if (micros() - pingTimer >= pingTimeout){
    dnsFail = false;

    boolean DNSsuccess;
    if (useServer1) {                                       // Default to Google server but try the BBC server
      DNSsuccess = ether.dnsLookup(server1);                // if the DNS lookup fails. Alternate between
    } else {                                                // the two servers until DNS lookup succeeds.
      DNSsuccess = ether.dnsLookup(server2);
    }

    if (!DNSsuccess ) { 
      dnsErrCount++;                                               
       Serial.println(F("DNS failed"));                       
       lcd.setCursor(4,0);
       lcd.print(F("           "));
       lcd.setCursor(4,0);                                      
       if (useServer1 ) {                                   // Show which server DNS lookup has failed.
          lcd.print(F("!"));
          printProgStr(server1, '.', offset1, 11);          // Only show up to first dot and start at 'offset'
       } else {                                             // but only show a maximum of 11 characters.
          lcd.print(F("!"));
          printProgStr(server2, '.', offset2, 11);
       }
       dnsFail = true;                                      // As DNS is now failing, we can increase the error count rate...
       if (errCount < 99)                                   // .. by counting DNS failures as well as ping failures.
           errCount++;
       if (!serverSwapped && (dnsErrCount >= 2)) {    
          useServer1 = !useServer1;                         // Swap to the secondary server after 2 DNS failures.
          serverSwapped = true;
          serverTimer = millis();                           // Start server timer running.
       }
                                  
    } else {
        lcd.setCursor(0,0);
        if (errCount < 10) lcd.print(F(" "));
        lcd.print(errCount);
        lcd.print(F("> "));
    }

    if (cleared) {                                                // If 2nd LCD row is not showing date/time...
      lcd.setCursor(5,1);

      useServer1 ? lcd.write(toupper(pgm_read_byte_near(server1+offset1))) :   // Show 1st letter of server that's being used.
                   lcd.write(toupper(pgm_read_byte_near(server2+offset2)));
   }
    
    ether.printIp("Pinging ", ether.hisip);                 // show ping address on Serial monitor
    
    if (!dnsFail)                                           // No point sending ping request if DNS has failed.
       ether.clientIcmpRequest(ether.hisip);                // Send the ping request and..
    
    pingTimer = micros();                                   // ..set the ping timer running
    errTimer = micros();                                    // start ping error timer
  }  


   word len = ether.packetReceive();                        // go receive new packets
   word pos = ether.packetLoop(len);                        // respond to incoming pings

    
  
/* ========== Report successful ping response on LCD ================================================== */
  
  if (len > 0 && ether.packetLoopIcmpCheckReply(ether.hisip)) {
    float temp = (micros() - pingTimer) * 0.001;
    lcd.setCursor(4,0);
    lcd.print(F("         ^"));
    lcd.setCursor(4,0);
    lcd.print(temp, 2);              
    Serial.print(temp, 3);   

    useServer1 = constrain(EEPROM.read(5), 0, 1);           // Swap back to main server
    serverSwapped = false;
    dnsErrCount = 0;
    
    Serial.println(F(" ms"));                               // Cosmetic text on Monitor and LCD.
    lcd.print(F("ms "));

    lcd.setCursor(0,0);
    lcd.print(F(" 0")); 
    lcd.print(F(">"));
    
    if (errCount > maxError) {                              // There must have been a router power-cycle
 
        if (nma && !timedLockout && !manualLockout ) {    
           nmaAction = 1;              
           notifyMyAndroid(0);                              // 'Router power-cycled' message.
        }
        if (!timedLockout) 
           lockoutTimer = millis();                         // Restart the timed lockout timer so lock time starts now
        timedLockout = true;
    }
    errCount = 0;                                           // Reset error count to zero for every successful response
    dnsErrCount = 0;
  }     
  
  
 /*  ========  No reply to ping after 'errTimeout' time so increment error counter. ================ */
  if  (micros() - errTimer > errTimeout) {
    if (errCount < 99)                                 // No response after error timeout so increment error counter.
       errCount++;                                
    Serial.print(F("Error "));                   
    Serial.println(errCount);  
    
    lcd.setCursor(0,0);                                // Update the LCD with the error count
    if (errCount < 10) lcd.print(F(" "));
    lcd.print(errCount);
    lcd.print(F(">"));
    lcd.setCursor(12,0);  
     
    if (!dnsFail) {                                    // If we have a ping response failure but NOT a DNS failure..
      if (errCount < maxError)                         // If the error was not due to a DNS lookup error, keep
         errHighCount = max(errHighCount, errCount);   // a tally of the highest error count before receiving
                                                       // a ping response. This should help setting the value at
                                                       // which to trigger the router power-cycle and the subsequent
      lcd.print(F(" ^")); 
      lcd.print(errHighCount);             

      lcd.print(F(" "));
    }
    
    errTimer = micros();                            // Re-start ping error timeout timer.
     
    if (!isLocked && (errCount >= maxError)) {      // After  'maxError' error timeouts or more...
      errCount++;                                   // Increment counter to ensure we only enter this loop once.
      
      if (!timedLockout && !manualLockout) {        // If router hasn't been power cycled in the last hour (or more, as set)..
        digitalWrite(lockLED, HIGH);                // 'Lock' LED ON.
        if (serverSwapped) {
          useServer1 = constrain(EEPROM.read(5), 0, 1);     // Swap back to main server
          serverSwapped = false;
          dnsErrCount = 0;
        }
        if (cleared) {                              // If 2nd row on LCD is not showing date/time, print the key character.
          lcd.setCursor(9,1);
          lcd.write(0);  
        }       
        digitalWrite(powerRelay, HIGH);             // Power down router. Router is wired to normally closed contact on
        digitalWrite(PowerLED, LOW);                // relay so energizing the relay breaks power to the router.  This is better
        delay(routerDelay);                         // than having the relay energized most of the time and is fail-safe if the
        digitalWrite(powerRelay,LOW);               // ATmega328 output fails.
        digitalWrite(PowerLED, HIGH);
        lockoutTimer = millis();                    // Initialize lockout timer so it won't time out next time round the main loop.
        isLocked = true;
      } 
    }
  } 
    

/* ===========   Look for a response from NotifyMyAndroid and extract the Date & Time from the returned reply  === */
  
  ether.packetLoop(ether.packetReceive());  
  
  char *reply = ether.tcpReply(session);              // Look out for session ID which was obtained from tcpSend.

  if (reply != 0) {                                   // Response received. 
    
    if (nmaAction == 1) {                             // Only parse date/time for LCD when the notification is from a router
      lcd.setCursor(0,1);                             // power cycle, not when notification for the timed lockout ends.
      dateLine = true;  

      /* The format of the reply is:                                                                                              *
       *  HTTP/1.1 200 OK<crlf>Server: nginx/0.8.55<crlf>Date: Sun, 07 Aug 2016 13:30:14 GMT<crlf>Content-Type...etc              *
       *                                     Location of ^D+11=idx--^   idx+12--^                                                 */
     
      char *p = strstr_P(reply, PSTR("Date"));        // Look for "Date" in reply and set pointer to start of actual date, 11 
      byte idx = (p - reply) + 11;                    // characters later. (Date/time is extracted from HTTP response header.)

      byte idy = 0;                                     
      for (byte i = idx; i < (idx+7); i++)  {         // Extract the actual date characters from the NMA reply.
        lcd.write(reply[i]);                          // and write to the LCD
        datetime[idy] = reply[i];                     // Copy date into byte array for recall later
        idy++;
      }
                                          
      for (byte i = (idx+12); i < (idx+17); i++)  {   // Extract hours and mins from NMA's reply for LCD  (skip the year). 
        lcd.write(reply[i]);                          // Write the time to the LCD
        datetime[idy] = reply[i];                     // Copy time into byte array for recall later
        idy++;
      }


      char dow[4];                                    // Extract day of week text from NMA reply

      dow[0] = reply[idx-5];
      dow[1] = reply[idx-4];
      dow[2] = reply[idx-3];
      dow[3] = '\0';
      
      for (byte i=0;i<7;i++) {                                    // Convert day of week text to numerical value
          if (strcmp_P(dow, pgm_read_word (&(dows_table[i] )))==0){
            nDow = i+1;                                           // result = numerical Day Of Week.
            break;
        }
      }


      char day[3];


      day[0] = reply[idx];                                        // Numerical day of month
      day[1] = reply[idx+1];
      day[2] = '\0';
      nDay = atoi(day);
      

      char mon[4];
      mon[0] = reply[idx+3];                                      // Month (text)
      mon[1] = reply[idx+4];
      mon[2] = reply[idx+5];
      mon[3] = '\0';

      for (byte i = 0; i<12;i++) {                                // Convert Month in NMA reply to numerical value.
        if (strcmp_P(mon, pgm_read_word (&(mons_table[i])) )==0){
           nMon = i+1;
           break;
        }
      }      

      switch (ds) {                                               // Check setting of Daylight Saving selector
        case 0: daylightSaving = false;                           // No, Yes, Auto
                break;
        case 1: daylightSaving = true;
                break;
        case 2: daylightSaving = isSummer();
                break;
        default: daylightSaving = false;
                 break;
      }
         

      lcd.setCursor(12,1);
      lcd.print(F(" "));
      datetime[12] = ' ';         
      
      if (daylightSaving) {                                      // During Daylight Saving, add an hour and print 'bst'...
        addHour();
        lcd.print(F("bst"));
      } else {
         lcd.write(3);
         lcd.setCursor(14,1);
         lcd.print(F("mt"));                                     // ... otherwise, print 'gmt'
      }
    }

    if (nmaAction == 2) {                                        // This is the second Notification (Router unlocked) 
       lcd.setCursor(0,1);
       for (byte i=0;i<16;i++)
         lcd.print(F(" "));                                      // Clear the LCD 2nd row
       lcd.setCursor(0,1);
       for (byte i=0; i<13; i++)
         lcd.write(datetime[i]);                                 // Recall date/time
         
       if (daylightSaving)                                       // Add daylight saving if required.
         addHour();   

       lcd.setCursor(13, 1);
       daylightSaving ? lcd.print(F("BST")) : lcd.print(F("GMT"));    // Change gmt or bst to Uppercase.
    }
    
    nmaAction = 0;                                   
    cleared = false;                          // Flag that 2nd row on LCD shows date/time.       
    Serial.print(reply);                      // Show full response from Notify My Android on Monitor.
    digitalWrite(nmaLED, LOW);                // NMA has responded so turn off LED indication that NMA was called.
  }
  
}

// stackoverflow.com/questions/5590429/calculating-daylight-saving-time-from-only-date

static bool isSummer() {
   if (nMon < 3 || nMon > 10)  return false; 
   if (nMon > 3 && nMon < 10)  return true; 

   int previousSunday = nDay - nDow;

   if (nMon == 3) return previousSunday >= 25;
   if (nMon == 10) return previousSunday < 25;

   return false; 
  
}

_________________________________________________________________________________________________________________________

Sketch uses 21,316 bytes (66%) of program storage space. Maximum is 32,256 bytes.
Global variables use 1,629 bytes (79%) of dynamic memory, leaving 419 bytes for local variables. Maximum is 2,048 bytes.
Low memory available, stability problems may occur.

 

Back to Index

 


This site and its contents are © Copyright 2015 - All Rights Reserved.