Projected Date (calculates the date in X days time)

0028_Projected_Date

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.

0028_Projected_Date_internal_copy

 

Schematic

Schematic_-_0028_Projected_Date

 

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 StringByVal OldValue As WordByVal Min, Max As WordByVal 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 <= (* 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 5 months ago by Leo #13646
Leo's Avatar
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.

Forum Activity

Member Access