GPS Clock Version 3 Part 4

Well in my last post I didn't quite get finished with what I had planned. No matter. Onward! Today I'm going to start with adding the DateTime class to the project and integrating that with the RTC class.

I've used a different method here. I've copied DateTime.h and DateTime.cpp from GPS Clock Version 2 into my current Version 3 project. The Arduino application doesn't notice the change until the project is re-opened. You can download the starting code for today here.

DateTime Class


Last time I explained that a header file (.h) is the 'public face' of a class. Let's have a look at DateTime.h and see what it publicises about itself. Remember this is a DateTime class I made myself, so I give no guarantees!

The private: section has three lines. The first allocates memory for seven integer variables. You can see that this is how a DateTime object keeps track of the date and time. These are private so they cannot be interfered with by code outside the class. If you want to set or change these values, you'll have to find a method in the public: section to do it with. There are also two parse methods that are private. There must be a reason these are private, but I don't remember what that is right now. It's likely that I decided (for performance) to limit the amount of checking I did and therefore didn't consider them 'safe' for other classes to use. So they remain accessible only to code inside the DateTime class.

In the public: section there are two enumerations, Period and DayOfWeek. These enumerations were added to make the code read a bit better. Internally to the code each enumeration value resolves to an integer value. Year = 0, Month = 1, etc. So they can be treated just like constant integers. With enumerations you can do things like:

  now.add(1, DateTime::Second);

... and ...

  if(now.dayOfWeek() == DateTime::Saturday)
    Serial.println("No work today!");

Next in the public: section are three constructors. My C programming's a bit vague when it comes to the differences with C++. I think constructors are a C++ concept that don't exist in C. Anyway, in C++, when an object of a class is being instantiated (created), a constructor is called. Interestingly, if you write a class that doesn't have a constructor, the C++ compiler will assume how to create an object and define a default constructor for you! This has caught me out a lot. The behaviour the compiler implements when making a default constructor is sometimes not what you want. If your class uses pointers, it's almost definitely not what you want! So the three different constructors, in order do these things:
  1. Create a DateTime object with no specific date and time
    Actually it's a good one to look in the code. Find DateTime::DateTime() in DateTime.cpp. You can see I've used an arbitrary date.
  2. Create a DateTime object from a character array for date and a character array for time.
  3. Create a DateTime object from separate integers representing each value.
The next set of methods are defined const. This means using these methods don't change the object they belong to. They're read-only. To help me with my LCD clock I created a set of methods for Tens and Units of hours and minutes. This really isn't a good use of the class. Something that's needed for the LED display should be in the LED class. Maybe we'll clean that up.

Below that are a set of methods that I won't describe in detail. Important, however, is the toLocal() method. Using this assumes the DateTime object has been set to UTC time and then toLocal() converts that to the local time. Just perfect for converting UTC time from the GPS into local time for display on a clock.

DateTime DateTime::toLocal()
{
  DateTime local(*this);
  DateTime dstStart;
  DateTime dstEnd;
  DateTime::getDaylightSavingsDates(*this, dstStart, dstEnd);
  if (this->isEarlierThan(dstStart) && (dstEnd.isEarlierThan(*this) || (dstEnd.isEqualTo(*this))))
    local.add(10, DateTime::Hour);
  else
    local.add(11, DateTime::Hour);
  return local;
}

This guy, along with getDaylightSavingsDates() is hard coded to work for Australian Eastern times (New South Wales, Victoria and Tasmania). If you're elsewhere in the world, you might want to change these two functions to pick the right dates and offsets to calculate your correct local time from UTC time. The methods isEarlierThan() and isEqualTo() are used by toLocal() to work out if the current date/time is within a daylight savings period.

The last set of five methods are operator overloads. This (I think) is again a C++ concept. I'm sure some people hate that you can overload operators, but I love it! Having defined these operators I can do things like this:

DateTime startDate(2012, 10, 6, 5, 37, 0, 0);
DateTime endDate(2012, 09, 6, 5, 37, 0, 0);
if (endDate < startDate)
  Serial.println("You can't have the end date before the start date!");

All right, that's a quick rip through the DateTime class. See how we (mostly) concentrated on the header file? That's what it's for.

Integrating DateTime and RTC Classes


Now that the DateTime class is available in the project, we need to integrate it. The first port of call is the RTC::getDateDs1307() method. This looks to me a very 'C' implementation of a method. Its arguments are pointers to byte variables (effectively making all those arguments outputs from the method) when what it really should do (in C++ terms) is return a single object that represents date and time information. So let's add a method to the RTC class to return a DateTime. Before we do that I've just compiled (verified) the project to make sure something hasn't gone wrong.

In the public: section of the RTC class' header file, I've added a new declaration:


  DateTime getDateTime();

The method will return a DateTime object. It's called getDateTime(). It has no arguments. That's the declaration. If I compile the project it fails right here: 'DateTime' does not name a type. Again, we haven't #included it. So at the top of RTC.h, with the other #include lines we need #include "DateTime.h". Compile again - we're good! Now we need to code the method. Copy the declaration line for getDateTime() and paste it into RTC.cpp at the bottom of the file.

We need to modify the declaration so we can use it for the implementation of the method. RTC:: must precede the method name and the trailing semicolon must be replaced with an opening and closing curly brace set that will hold the method's code.

DateTime RTC::getDateTime()
{
}

Now, we have a fundamental difference in how data is being handled. The existing methods in the RTC class are using the byte data type to store info. DateTime uses int. Usually we can cast between the two and get away with it - but there might be trouble! By 'cast' I mean one data type is converted into a compatible type without even writing any code to do it - an 'implicit cast'. In RTC::getDateDs1307() the method Wire.read() is used to get data from the RTC hardware. I wonder, does that return bytes or ints? I went to my hard disk to find an answer (on my 64-bit Windows 7 system: "C:\Program Files (x86)\arduino-1.0.1\libraries\Wire\Wire.h".

This is probably too much depth for many. Let's see. In Wire.h we find there's one read() method and it returns int. That seems simple. But since we're reading and writing I think we should check the write method too. Hmm, there are two of them. One takes a single uint8_t (unsigned integer of 8 bits _type), the other takes a pointer to a uint8_t and a size_t. That's basically a character array: a pointer for the start and a size_t to give the length in bytes. But wait, there's more! At the bottom are a set of inline size_t write() methods. These accept different data types (unsigned long, long, unsigned int and int) and cast them all to a uint8_t before passing them on to the original write() method for uint8_t values. So, if I understand it, when I read() I get an int back. When I write() I can write any of four types but they'll be cast to an 8-bit value. What happens if a byte can't store the value I had in a different data type? I guess it's truncated and the program develops a bug. This doesn't seem right to me. This is definitely 'risky' code.

I now know I can interact with the Wire library using integers. So we'll implement RTC::getDateTime() along the same lines as RTC::getDateDs1307(). I've copied the code with a few changes:
  • The first four lines are unchanged
  • I added a line to declare all the variables inside the method that used to be declared in the method declaration as arguments
  • I modified all the assignment lines (with bcdToDec() in them) to remove the star (*) at the start. These are now local variables, not pointers.
  • I added two lines at the end to create a new DateTime object called rtcNow and return it from the method.
The full method is below:

DateTime RTC::getDateTime()
{
  Wire.beginTransmission(DS1307_I2C_ADDRESS);
  Wire.write(0);
  Wire.endTransmission();

  Wire.requestFrom(DS1307_I2C_ADDRESS, 7);
  int second, minute, hour, dayOfWeek, dayOfMonth, month, year;
  
  // A few of these need masks because certain bits are control bits
  second     = bcdToDec(Wire.read() & 0x7f);
  minute     = bcdToDec(Wire.read());
  hour       = bcdToDec(Wire.read() & 0x3f);  // Need to change this if 12 hour am/pm
  dayOfWeek  = bcdToDec(Wire.read());
  dayOfMonth = bcdToDec(Wire.read());
  month      = bcdToDec(Wire.read());
  year       = bcdToDec(Wire.read());
  
  DateTime rtcNow(year, month, dayOfMonth, hour, minute, second, 0);
  return rtcNow;
}

Do you see what's missing in the creation of the rtcNow object? It doesn't use the dayOfWeek that was received from the RTC. Why? Because day of the week can be calculated from the date. The DateTime class does this when you call the dayOfWeek() method. It looks like the RTC doesn't have this capability and relies on the programmer to set the day of the week. Then it just does a plus 1 mod 7 calculation each time the clock rolls over to another day. Do you see what they did there? The makers of the DS1307 chip either couldn't fit the code into the chip to handle day-of-the-week internally, or they decided to drop the logic for this to make the chip simpler and therefore cheaper. But less work for them is more work for us!

Now we can go back to void setup() where we pasted in that great ugly chunk of code (previous blog post) and delete it out. The whole main sketch now looks like this:

#include "RTC.h"
#include "Wire.h"
#include "DateTime.h"
void setup() {   Serial.begin(115200);   Serial.println("setup()");      Wire.begin();   RTC rtc;   DateTime now = rtc.getDateTime();   now.print();   Serial.println(); } void loop() {   Serial.println("loop()");   delay(1000); }
One of the big benefits of using classes in Arduino development is now apparent. Do you see how little code there is in the main sketch? I know you know how much work is going on in the code, but it's hidden under the covers now. When you want to understand a sketch, and the main program is about ten lines long, you grok it fast! This sketch says:
  1. Wire.begin()
    Get the Wire library started, it's necessary for something.
  2. RTC rtc;
    Make an object that refers to the real-time clock
  3. DateTime now = rtc.getDateTime();Get the date and time out of the RTC thingy and name that 'now'.
  4. now.print()Print out the date and time
Well, previously we had the interrupt working to print the date and time out every second. Let's do that again. I'm looking back at the Version 3 Part 2 post to find the syntax of the attachInterrupt() statement and the function it called. Here's the updated main sketch:

#include "RTC.h"
#include "Wire.h"
#include "DateTime.h"
RTC rtc; boolean interruptComplete = false; void setup() {   Serial.begin(115200);   attachInterrupt(0, interruptHandler, FALLING); } void interruptHandler() {   interruptComplete = true; } void loop() {   if (interruptComplete)   {     DateTime now = rtc.getDateTime();     now.print();     Serial.println();     interruptComplete = false;   } }
Let's walk through the code. The #include lines reference the external files we need. Because the rtc object is being referenced in more than one function, we've moved it out to global scope by declaring it outside any functions. There's also the interruptComplete boolean flag we used before. setup() is now only responsible for kicking off the Serial port and attaching the interruptHandler. Where did the Wire.begin() line go? I've created a constructor for the RTC class and moved that line into the constructor (check RTC.h and RTC.cpp). When the rtc object is created, it kicks off the Wire library by itself. The super simple interruptHandler() (renamed from "blink", which is not a descriptive name) does what it did before and loop() once again looks at the interruptComplete flag to determine if it should grab the current time and display it. That's it! Full code here.

Output looks like you'd expect, one line per second:


12-04-01 04:15:09.000
12-04-01 04:15:10.000
12-04-01 04:15:11.000
12-04-01 04:15:12.000
12-04-01 04:15:13.000
12-04-01 04:15:14.000
12-04-01 04:15:15.000
12-04-01 04:15:16.000


Obviously the date and time above are wrong. But we're going to use the GPS to set that. Next time!

That is all.

Comments