Low-memory footprint, scheduler-friendly web time client

This is an routine to obtain the Unix time by accessing a web server. This is an alternative to using an NTP client that has the same performance as long as no sub-second accuracy is needed. The idea is that we access a web server and read the Date: header that HTTP 1.1 compliant server will have to send.

This server uses very little memory and can easily be adapted to cooperative scheduling environments.

Concept

HTTP 1.1 mandates servers to send a Date: header in a standard format. This header must be sent even if the server receives an invalid request, as long as it is recognised as an HTTP 1.1 request. So we just send a line followed by two carriage return - newline sequences and read the answer, looking for a line beginning with Date: . The request is built like this:

GET / HTTP/1.1


The resulting answer is parsed with code that is very small and efficient.

Cooperative scheduling

Putting a yield() call after each WiFiUdp.method() call will make this code friendly to cooperative scheduler environments.

Invocation

For WiFi shield:

 {
   static WiFiClient client;
   unsigned long unixTime = webUnixTime(client);
 }

For Ethernet shield:

 {
     EthernetClient client;
     unsigned long unixTime = webUnixTime(client);
 }

Code

/*
 * © Francesco Potortì 2013 - GPLv3
 *
 * Send an HTTP packet and wait for the response, return the Unix time
 */

unsigned long webUnixTime (Client &client)
{
  unsigned long time = 0;

  // Just choose any reasonably busy web server, the load is really low
  if (client.connect("g.cn", 80))
    {
      // Make an HTTP 1.1 request which is missing a Host: header
      // compliant servers are required to answer with an error that includes
      // a Date: header.
      client.print(F("GET / HTTP/1.1 \r\n\r\n"));

      char buf[5];			// temporary buffer for characters
      client.setTimeout(5000);
      if (client.find((char *)"\r\nDate: ") // look for Date: header
	  && client.readBytes(buf, 5) == 5) // discard
	{
	  unsigned day = client.parseInt();	   // day
	  client.readBytes(buf, 1);	   // discard
	  client.readBytes(buf, 3);	   // month
	  int year = client.parseInt();	   // year
	  byte hour = client.parseInt();   // hour
	  byte minute = client.parseInt(); // minute
	  byte second = client.parseInt(); // second

	  int daysInPrevMonths;
	  switch (buf[0])
	    {
	    case 'F': daysInPrevMonths =  31; break; // Feb
	    case 'S': daysInPrevMonths = 243; break; // Sep
	    case 'O': daysInPrevMonths = 273; break; // Oct
	    case 'N': daysInPrevMonths = 304; break; // Nov
	    case 'D': daysInPrevMonths = 334; break; // Dec
	    default:
	      if (buf[0] == 'J' && buf[1] == 'a')
		daysInPrevMonths = 0;		// Jan
	      else if (buf[0] == 'A' && buf[1] == 'p')
		daysInPrevMonths = 90;		// Apr
	      else switch (buf[2])
		     {
		     case 'r': daysInPrevMonths =  59; break; // Mar
		     case 'y': daysInPrevMonths = 120; break; // May
		     case 'n': daysInPrevMonths = 151; break; // Jun
		     case 'l': daysInPrevMonths = 181; break; // Jul
		     default: // add a default label here to avoid compiler warning
		     case 'g': daysInPrevMonths = 212; break; // Aug
		     }
	    }

	  // This code will not work after February 2100
	  // because it does not account for 2100 not being a leap year and because
	  // we use the day variable as accumulator, which would overflow in 2149
	  day += (year - 1970) * 365;	// days from 1970 to the whole past year
	  day += (year - 1969) >> 2;	// plus one day per leap year 
	  day += daysInPrevMonths;	// plus days for previous months this year
	  if (daysInPrevMonths >= 59	// if we are past February
	      && ((year & 3) == 0))	// and this is a leap year
	    day += 1;			// add one day
	  // Remove today, add hours, minutes and seconds this month
	  time = (((day-1ul) * 24 + hour) * 60 + minute) * 60 + second;
	}
    }
  delay(10);
  client.flush();
  client.stop();

  return time;
}

Share