## Projected Date (calculates the date in X days time)

OK, so this project might not be a killer robot or auto-guided missile, though for what ever reason it has turned out to be a decently sized program! The scenario that brought this project to life would most likely be quite unique and rarely encountered, though I thought it would still be worth sharing as the program has a few handy little features and nifty tricks.

## Background

I fix/maintain a lot of gear at work that revolves around what is known as "Scheduled Servicing". Nothing new here - if an item has a 60 day Bay Service or Calibration, then every 60 days that servicing is carried out. Some are 30 days, others are 120 days and so forth. There's a problem here though - my work environment complies with Aeronautical AMO regulations (Approved Maintenance Organisation), a very thorough set of regulations/practices to keep a safe and accountable work environment.

I've recently discovered a deficiency with day type calculates for scheduled servicing. Consider the date is 25-Jul-2010, and you've just completed a calibration on a bit of gear that is due again in 60 days. When completing the paperwork, one would normally assume "60 days is two months" and write up the paperwork accordingly (next CAL due 25-Sep-2010). Heaven-behold anyone who uses the item from 23rd - 25th of September is actually in violation of a couple of regulations.

Why? Well the answer has most likely already hit home to most people by now - not every month has 30 days in it. Not only does this highlight possibility to unwillingly violate AMO regs, though there is the opportunity to slightly increase productivity (365 days/12 months = 30.42 days, not 30).

## Solution(s)

First option is a gimme - Just learn how many days in every month and accommodate for leap years etc... Easy enough, though I would forget it in about 32 minutes.

Second option - use an Excel spreadsheet and calculate the date. For example, the above scenario would be easily solved with the following expression "=DATE(2010,7,25)+60". Excel would display "23/09/2010" in the cell. Given we only have one PC at the end of our work area, it seems a mundane task to stop you in your tracks just to calculate a date.

Third option - make a box that does it for me and can be setup in-situ for easy use. Seems a whole lot of work for such a small deal, though I had nothing on during a Sunday arvo - and got to it.

## Schematic

I'm using a DS1307 RTC (Real Time Clock) as the backbone of time/date keeping. There are a handful of benefits with using the device over a dedicated PIC program - the biggest one is probably a lithium battery with 48mAh or greater will back up the DS1307 for more than 10 years in the absence of power. Not that I plan on using the device once every 10 years - though keeping the cost of consumables down is definitely on the agenda. I'm using two AA lithium batteries as a backup power source on the DS1307 - purely to prevent the loss of data in-case of power interruption. They also allow the unit to be unplugged, transported and reconnected without re-configuring settings (even stored in a draw for a week or two).

## User Interaction

The first revision of the project used two switches. One for changing the Mode (projected date forecast), and the other to initiate settings configuration (to set the time/date). It soon became apparent that hitting away on a single button to increment variables was quite annoying - I did want to make something usable by the lowest common denominator at the end of the day. What I trialed and settled on was one button and a potentiometer. I ventured into an interesting method of interaction that will probably form the basis of my future designs.

Before I get to in-depth, consider the normal mode of operation - date projection. I want a box that will tell me what date it is in 30, 60, 120 days time (there's a few other ones, though I'll keep it easy). So, in normal operation, Mode One could display the date in 30 days time, Mode Two in 120 days time and so on. What if I put together a program that automatically calculates how many modes there are, and turns a 10-bit ADC result from the potentiometer into segments for different modes.

For example, I have three modes (30, 60, 120 days) - now simply break the 10-bit potentiometer result into 3 segments (1023/3=341). Therefore, a reading from 0-341 would imply Mode One, 342 to 682 is Mode Two and 683 to 1023 would be Mode Three. I know I'm referring to the processes as "Modes", its more so because the same approach can be transposed into different scenarios quite easily.

"Jittering??" I here some people yell out. For those that didn't yell out, perhaps this would trigger a thought - what happens if the potentiometer is biased at a point of change, such as hovering between 341 and 342. This would make the program jump between Mode One and Mode Two rapidly.... There are a number of methods that can be used to completely remove jitter, and in all honesty I did not experience any "jumpiness" even without the methods employed. First of all, build a stable voltage regulator with all the normal trimmings. Decent input/output caps, a decoupling cap as-close-as-possible to the PIC's Vdd/Vss. The additional method I took was taking many ADC readings and calculating the Median.

Finding the Median from a pool of samples is different to averaging. Consider some ADC samples; 512, 512, 511, 512, 1023. There are 5 readings, they appear to be sitting at about 512, though there is a noise spike which returned 1023. Calculating the average would include the undesirable noise (512+512+511+512+1023) / 5 = 614.
The Median can be found by arranging the results from smallest to largest, and then picking the sample in the middle. In this case, 511, 512, 512, 512, 1023. Our result, 512. This is a much more accurate method of sampling analogue signals, though it takes a little more time. I did have my own function to do this very job, though Swordfish already has such a function as part of the ADC.bas library; ADC.ReadMedian(ChannelNumber). It will take 64 samples and return the Median.

With the above considerations in place, your left with a blissfully easy-to-use and creative method for end users to interact. Here's a video of the unit in use;

How about I break things up a little bit and share the whole program (more tips below the code)

Project The Date (Source Code)
```// Purpose: To project the date in X days time.
// Notes:   Edit the constant PresetDays(x) to configure preset date ranges.
//          While the DS1307 offers leap year compensation as an RTC, this program
//          accommodates leap years with date forecasting as required.
//
//          There are interlocks to ensure safe operation (when setting time/date for example).
//
//          Edit the constant PresetDays(x) with careful attention to setting the array index
//          size. By doing so, the program will automatically use the preset date forecast
//          ranges.

Device = 18F2620
Clock = 32
Config OSC = INTIO67

#option LCD_DATA = PORTB.0                  // define custom LCD configuration
#option LCD_EN = PORTA.1                    //
#option LCD_RW = PORTA.2                    //
#option LCD_RS = PORTA.3                    //

Include "INTOSC8PLL.bas"                    // this is a User Library that configures the INT OSC to 8Mhz on powerup
Include "Convert.bas"                       // other system libraries used
Include "LCD.bas"                           //
Include "I2C.bas"                           //
Include "ADC.bas"                           //

// this constant is really the only one that needs to be changed. by default,
// Const PresetDays(7) as Word = (15,30,45,60,90,120,365) defines date projection
// with 15, 30, 45... days. Perhaps the end user does not require 365 days projection,
// simply change the constant to: Const PresetDays(6) as Word = (15,30,45,60,90,120)
// The program will handle the rest automatically. Nifty.
Const PresetDays(8) As Word = (15,30,45,60,90,120,180,365)

// program constants
Const DaysOfMonth(12) As Byte = (31,28,31,30,31,30,31,31,30,31,30,31),
DaysOfWeek(7) As String = ("Mon","Tue","Wed","Thu","Fri","Sat","Sun"),
ModeChannel = 0
// program variables/defines
Dim Day_Poll As Byte,
ReCalc As Boolean,
Mode As Word,
SetDateTime As PORTB.4
// program structured variables
Structure TTime
Second As Byte           // Second (0..59)
Minute As Byte           // Minute (0..59)
Hour As Byte             // Hour   (0..11 or 0..23)
End Structure
Dim Time As TTime

Structure TProjectedDate
Day   As Byte            // Date  (0..31)
Month As Byte            // Month (1..12)
Year  As Byte            // Year  (0..99)
DayOfWeek As Byte        // day of the week (1..7)
End Structure
Dim ProjectedDate As TProjectedDate

Public Structure TDate
Day   As Byte            // Date  (0..31)
Month As Byte            // Month (1..12)
Year  As Byte            // Year  (0..99)
DayOfWeek As Byte        // day of the week (1..7)
End Structure
Dim Date As TDate

// this function checks if the passed variable Year is in fact a leap year or not.
Function LeapYear(ByVal Year As Byte) As Boolean
Year = Year + 2000                      // scale to correct date
Result = False                          // reset the function result
If Year Mod 4 = 0 Then                  // Mod returns the remainder of a division
Result = True
If Year Mod 100 = 0 Then
If Year Mod 400 <> 0 Then
Result = False
EndIf
EndIf
EndIf
End Function

// increment by one day, handling month/year rollovers at the same time
Sub IncrementDay(ByRef Day, Month, Year, MaxDays As Byte)
If Day = MaxDays Then
Day = 1
If Month = 12 Then
Month = 1
Inc(Year)
Else
Inc(Month)
EndIf
Else
Inc(Day)
EndIf
End Sub

// increment the day of week
Sub IncrementDayOfWeek(ByRef DayOfWeek As Byte)
Inc(DayOfWeek)
If DayOfWeek = 8 Then
DayOfWeek = 1
EndIf
End Sub

// this is the higher sub routine for incrementing days.
// it utilizes the above subs to perform functions and processes
Sub CalculateProjectionDate(ByVal ProjectedDays As Word)
Dim LastDay As Byte
ProjectedDate.Day = Date.Day
ProjectedDate.Month = Date.Month
ProjectedDate.Year = Date.Year
ProjectedDate.DayOfWeek = Date.DayOfWeek
While ProjectedDays > 0
If LeapYear(ProjectedDate.Year) Then
If ProjectedDate.Month = 2 Then
LastDay = 29
Else
LastDay = DaysOfMonth(ProjectedDate.Month-1)
EndIf
Else
If ProjectedDate.Month = 2 Then
LastDay = 28
Else
LastDay = DaysOfMonth(ProjectedDate.Month-1)
EndIf
EndIf
IncrementDay(ProjectedDate.Day,ProjectedDate.Month,ProjectedDate.Year,LastDay)
IncrementDayOfWeek(ProjectedDate.DayOfWeek)
Dec(ProjectedDays)
Wend
End Sub

// DS1307 set time sub routine
Sub SetTime(ByRef Hour, Minute, Second, DayOfWeek, Day, Month, Year As Byte)
I2C.Start
I2C.WriteByte(%11010000)             // Send the RTC address, and put it in write mode
I2C.WriteByte(\$00)                   // Move the pointer to first register
I2C.WriteByte(DecToBCD(Second))      // Write each byte
I2C.WriteByte(DecToBCD(Minute))      //
I2C.WriteByte(DecToBCD(Hour))        //
I2C.WriteByte(DecToBCD(DayOfWeek))   //
I2C.WriteByte(DecToBCD(Day))         //
I2C.WriteByte(DecToBCD(Month))       //
I2C.WriteByte(DecToBCD(Year))        //
I2C.WriteByte(0)                     //
I2C.Stop
End Sub

// DS1307 GetTime sub routine
Sub GetTime()
I2C.Start
I2C.WriteByte(%11010000)
I2C.WriteByte(\$00)
I2C.Restart
I2C.WriteByte(%11010001)
Time.Second = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE))
Time.Minute = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE))
Time.Hour = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE))
Date.DayOfWeek = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE))
Date.Day = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE))
Date.Month = BCDToDec(I2C.ReadByte(I2C_ACKNOWLEDGE))
Date.Year = BCDToDec(I2C.ReadByte(I2C_NOT_ACKNOWLEDGE))
I2C.Stop
End Sub

// return a three letter month abbreviation
Function MonthToMMM(ByRef Month As Byte) As String(4)
Select Month
Case 1
Result = "Jan"
Case 2
Result = "Feb"
Case 3
Result = "Mar"
Case 4
Result = "Apr"
Case 5
Result = "May"
Case 6
Result = "Jun"
Case 7
Result = "Jul"
Case 8
Result = "Aug"
Case 9
Result = "Sep"
Case 10
Result = "Oct"
Case 11
Result = "Nov"
Case 12
Result = "Dec"
End Select
End Function

// debounce routine
// this routine was edited to provide greater switch debounce control
Sub Debounce(ByVal Period As Byte)
DelayMS(Period)
While SetDateTime = 0
While SetDateTime = 0
Wend
DelayMS(Period)
Wend
End Sub

// function that samples the Mode potentiometer and returns the desired decoded value
Function GetModePosition(ByVal Min, Max As Word) As Word
Dim Segments, i As Byte, Range As Word

Result = ADC.ReadMedian(ModeChannel)
Range = Max - Min
// split the 10 bit ADC resolution into known segments for the array
Segments = 1023 / (Range + 1)
// determine where the ADC result lies throughout the segments & return the desired array value
i = 0
Repeat
If Result <= ((i+1) * Segments) Then
Break
EndIf
Inc(i)
Until i = Range
Result = i + Min
End Function

// multi-purpose function that handles data collection from the Mode potentiometer.
// there are special considerations for DaysOfWeek and Year modes, to make the data
// more presentable to the end user.
Function GetNewVal(ByVal DisplayString As String, ByVal OldValue As Word, ByVal Min, Max As Word, ByVal Length As Byte = 2) As Word
Dim Update As Boolean, tmpPosition As Word

Result = OldValue
If Result > Max Then
Result = Max
ElseIf Result < Min Then
Result = Min
EndIf

tmpPosition = GetModePosition(Min,Max)
Result = tmpPosition
Update = True

Debounce(10)
LCD.WriteAt(2,1,"                ")
Repeat
If Update = True Then
If DisplayString = "DOW" Then
LCD.WriteAt(2,1,DisplayString,": ", DaysOfWeek(Result-1))
ElseIf DisplayString = "Yr " Then
LCD.WriteAt(2,1,DisplayString,": ", DecToStr(Result+2000,4))
Else
LCD.WriteAt(2,1,DisplayString,": ", DecToStr(Result,Length))
EndIf
Update = False
EndIf
tmpPosition = GetModePosition(Min,Max)
If Result <> tmpPosition Then
Result = tmpPosition
Update = True
EndIf
Until SetDateTime = 0
End Function

// sub routine that walks a user through setting the time.
// it emplaces interlocks to insure the user can not set the date/time
// beyond valid settings.
Sub SetTheTime()
LCD.Cls
LCD.WriteAt(1,1,"    Set Date    ")
Debounce(10)

Date.Year = GetNewVal("Yr ",Date.Year,10,99)
Date.Month = GetNewVal("Mth",Date.Month,1,12)
If Date.Month <> 2 Then
Date.Day = GetNewVal("Day",Date.Day,1,DaysOfMonth(Date.Month-1))
Else
If LeapYear(Date.Year) Then
Date.Day = GetNewVal("Day",Date.Day,1,29)
Else
Date.Day = GetNewVal("Day",Date.Day,1,28)
EndIf
EndIf
Date.DayOfWeek = GetNewVal("DOW",Date.DayOfWeek,1,7,1)

LCD.WriteAt(1,1,"    Set Time    ")
Time.Hour = GetNewVal("Hr ",Time.Hour,0,23)
Time.Minute = GetNewVal("Min",Time.Minute,0,59)
Time.Second = GetNewVal("Sec",Time.Second,0,59)

SetTime(Time.Hour, Time.Minute,Time.Second,Date.DayOfWeek,Date.Day,Date.Month,Date.Year)
LCD.Cls
LCD.WriteAt(1,1,"Saved")
DelayMS(2000)
End Sub

// monitors for a change in the date, if one occurs a recalculation is requested
Sub CheckForRefresh()
GetTime()
If Date.Day <> Day_Poll Then
ReCalc = True
Day_Poll = Date.Day
EndIf
End Sub

// handles the Mode potentiometer settings while in date projection mode.
// makes use of the preloaded constant array, and performs calculations to break the 10-bit ADC
// result into segments for desired settings.
Function GetDayMode() As Word
Dim Segments, ArrayBound, i As Byte

Result = ADC.ReadMedian(ModeChannel)
ArrayBound = Bound(PresetDays)
// split the 10 bit ADC resolution into known segments for the array
Segments = 1023 / (ArrayBound + 1)
// determine where the ADC result lies throughout the segments & return the desired array value
i = 0
Repeat
Inc(i)
If Result <= (i * Segments) Then
Break
EndIf
Until i = (ArrayBound + 1)
Result = PresetDays(i-1)
End Function

// monitor for a change with the Mode potentiometer
Sub CheckForModeChange()
Dim tmpWord As Word
tmpWord = GetDayMode
If tmpWord <> Mode Then
Mode = tmpWord
ReCalc = True
EndIf
End Sub

// monitor for a recalculation request, and handle if necessary
Sub CheckForRecalculate()
If ReCalc = True Then
CalculateProjectionDate(Mode)
LCD.Cls
LCD.WriteAt(1,1,"Date: ", DecToStr(Date.Day,2), "-", MonthToMMM(Date.Month), "-", DecToStr(Date.Year,2))
LCD.WriteAt(2,1,"+", DecToStr(Mode,3),": ",DecToStr(ProjectedDate.Day,2),"-",MonthToMMM(ProjectedDate.Month),"-",DecToStr(ProjectedDate.Year))
ReCalc = False
Debounce(10)
EndIf
End Sub

// monitor for a press of the Set Time switch.
// the user must hold the switch for ~2 seconds to enter the mode.
Sub CheckForSetTime()
Dim tmpByte, CurPos As Byte
If SetDateTime = 0 Then
LCD.Cls
LCD.WriteAt(1,1,"Set Time?")
LCD.WriteAt(2,1,"Hold button")
CurPos = 12
// gradually display "....." while the button is pressed.
// should the user depress the button before 2 seconds, the
// routine will break and request a recalculation
For tmpByte = 1 To 40
DelayMS(40)
If SetDateTime = 1 Then
Break
EndIf
If tmpByte Mod 5 = 0 Then
LCD.WriteAt(2,CurPos,".")
Inc(CurPos)
EndIf
Next
If tmpByte = 41 Then
SetTheTime()
EndIf
// request recalculation purely for LCD refresh
ReCalc = True
EndIf
End Sub

// start of program
OSCTUNE.6 = 1                               // enables PLL
ADCON1 = %00001110                          // channel 0 Analogue, everything else digital
SetConvTime(FRC)                            // using a 10K Potentiometer - FRC offers good accuracy at high impedance
I2C.Initialize()                            // set up the hardware I2C module

Input(SetDateTime)                          // SetDateTime - button used to enter set date/time mode
INTCON2.7 = 0                               // enable PORTB weakpullups (for use with the above input)

DelayMS(150)                                // allow the circuit to power up and stabilise
LCD.Cls                                     // splash screen
LCD.WriteAt(1,1," Projected Date ")         //
LCD.WriteAt(2,1," By Mitchy 2010 ")         //
DelayMS(2000)                               //
LCD.Cls                                     //

GetTime()                                   // get the current date/time from DS1307
Day_Poll = 0                                // reset the day poll register (used to restrict LCD updates)
Mode = GetDayMode                           // initialize the program with current status of controls
ReCalc = True                               // force the program to recalculate algorithms
CheckForRecalculate()                       //

While True                                  // main program loop
CheckForSetTime()                       // check to see if the time set button has been pushed
CheckForRefresh()                       // handles polling of seconds and days to control the refresh rate of the LCD
CheckForModeChange()                    // check if user has requested mode change
CheckForRecalculate()                   // check if any recalculations are ready, could occur from Date.Day change, or Mode change
Wend ```

## Leap Year Detection

OK, so thats a pretty big program to do something that appears to be simple enough. Another little trick I've picked up along the way with this project is calculating if its a leap year or not.

I always thought it was as easy as "if a year is divisable by 4 then its a leap year". Clearly I've been misslead at somestage! Here's the method;

1. If the year is evenly divisible by 4, go to step 2. Otherwise, go to step 5. 2. If the year is evenly divisible by 100, go to step 3. Otherwise, go to step 4. 3. If the year is evenly divisible by 400, go to step 4. Otherwise, go to step 5. 4. The year is a leap year (it has 366 days). 5. The year is not a leap year (it has 365 days).

In terms of program the above method into Swordfish;

```FunctionLeapYear(ByVal Year As Byte) As Boolean
Year = Year + 2000                      // scale to correct date
Result = False                          // reset the function result
If Year Mod 4 = 0 Then                  // Mod returns the remainder of a division
Result = True
If Year Mod 100 = 0 Then
If Year Mod 400 <> 0 Then
Result = False
endif
EndIf
EndIf
End Function```

I scale the year up by 2000 as the DS1307 works with a scale of -2000. For example, the year 2010 would be read as 10.

Thoughts and comments are more then welcome!

Posted: 5 years 6 months ago by Leo
Hi body, I take the code and I got a error in Swordfish:
Program variable allocation exceeds Swordfish Special Edition (SE) maximun.

Do you have the .hex file? Can you upload it? Thank you for the project.