Home >

Random Time Lock Device

time lock device with random countdown timer

This is my finished time lock device. It's based on a fairly primitive prototype I made back in 2011 which uses a high-ratio geared motor to operate a common padlock built into an enclosure.

Features include

I'm planning on adding a buzzer which sounds when the device has completed, or is about to perform, an unlock routine. The number and duration of buzzes, and whether they happen before or after the unlock routine, would all be configurable via the menu system.

time lock device, corner view time lock device, corner view time lock device, top view time lock, released / unlocked time lock internal, showing motors

My earlier design used a reed switch to determine how far to drive the motor/mechanism in either direction. This new design uses a current-sense resistor and op-amp to detect when a motor has stalled, having driven the mechanism to the end of its travel.

This design also incorporates two identical motorised padlocks, which offers a degree of redundancy as it's a design requirement that this device should ideally never fail to unlock. Operating each motor in turn, rather than simultaneously, means that only a single current-sense circuit is required. This one-at-a-time approach means that all the battery power is available to each motor, thus maximising the likelihood of the mechanism unlocking successfully.

underside of time lock display board

The display board is secured to the main PCB using extended pin headers. This allows the motors and batteries to fit snugly between the two boards. I didn't have the 33 ohm 0805 resistor (R3) handy at the time so had to resort to a regular one. This resistor limits the current to the display's backlight. 33 ohms was the value I settled on which struck a nice balance between brightness and power consumption.

schematic for time lock display board

The display board also carries the TSOP4838 infrared sensor for receiving data from the remote control, and the infrared phototransistor used to power on the device.

The 10k resistor (R1) is a MCLR pull-up to keep the microcontroller in an operational (non-reset) state. It's mounted on this board as there wasn't much space left on the mainboard.

The In Circuit Serial Programming header is passed-through from the mainboard for ease of connecting a PICkit programmer.

enclosure internal, with batteries enclosure internal, batteries removed time lock main printed circuit board schematic for time lock device

Directional control of both motors is provided by the L293D H-bridge driver IC. There are more sophisticated MOSFET based motor drivers that offer current sensing and are controlled serially, but I decided on the L293D for the sake of familiarity and simplicity.

Another design requirement was that the device should not have any physical buttons. Every function from powering on the unit to entering numeric values should be done using a remote control. The remote I chose is cheap and readily available, and all of the seventeen buttons serve some function when operating the device.

The P-channel power MOSFET (Q5) is used to soft-power the device on and off. Powering on is achieved using an infrared phototransistor mounted next to the display. When the remote is placed in close proximity to the phototransistor, pressing any button is sufficient to turn it on, which latches-on the MOSFET via an arrangement of NPN transistors.

Powering off is done via software. Either by selecting the 'POWER OFF' option from the menu, or the device will auto-power-off after a user specified period - assuming that no timer's running. A digital pin on the PIC18F drives the base of another NPN transistor which unlatches the MOSFET.

The Nokia 5110 LCD display is designed to operate at 3.3v, but has been known to work fine at 5v so long as current limiting resistors are placed on the data and control lines. I decided to operate the display at the prescribed 3.3v, provided by a voltage regulator placed after the MOSFET, which also powers the microcontroller and op-amp. The L293D is powered directly from the battery's nominal six volts.

As this device needs to unlock in the event of failure, it's necessary to regularly monitor the battery voltage. Which is another reason for operating the digital electronics at 3.3v, as it makes this so much easier. It's achieved by placing a voltage divider across the incoming supply, after the MOSFET, and feeding its output into one of the PIC18F's analogue inputs.

The device is powered using four AAA batteries. So a maximum of 1.65v * 4 / 2 gives a voltage to the Analogue to Digital Convertor of 3.3v, about equal to the PIC18F's supply voltage at VDD, which is also configured to be the ADC's voltage reference.

Having performed tests, I found that 5v is the minimum required to operate the motor so it can reliably actuate the padlock mechanism. The following code converts the ADC's 10-bit value to a percentage, where six volts and above equals one-hundred percent, and five volts or less equals zero.

  float v;
            
  v = 3.32 * ( adc_value / 1023 ) * 2;

  float v_minimum = 5.00;

  float v_nominal = 6.00;

  int pc_remaining = ( (v - v_minimum) / (v_nominal - v_minimum) ) * 100;

  if( pc_remaining > 100 ){ pc_remaining = 100; }

  if( pc_remaining < 0   ){ pc_remaining = 0;   }

  return pc_remaining;

  

If the percentage returned by this code - executed regularly whilst a timer's running - returns a zero, then the device will cancel the timer and call the unlock routine.

A second power MOSFET (Q6) is used to power the motor drive circuitry and L293D as this only requires power during the unlock routine. This keeps the total power consumption to ~10ma whilst the timer's running, which increases to ~20ma when the backlight is turned on.

The 32.768kHz watch crystal (Y2) is used to drive the PIC18F's Timer1 and triggers an interrupt at an accurate one second interval. The interrupt increments the seconds variable which drives the clock display.

motorised padlock

This is a modified ABUS Titalium padlock. A recess to accommodate the three sides of the cut-out in the lower portion of the enclosure have been milled in the lock's body, with a slightly wider recess on top to accommodate the mating edge of the acrylic lid.

The keyway cylinder, pins and springs have been removed to make way for a new brass cylinder machined to the same proportions that operates the internal release mechanism in a similar fashion.

brass cylinder beside original cylinder - showing tabs brass flat bar mounted to geared motor brass cylinder mounted to geared motor

This brass cylinder is secured to the geared motor's output shaft using a grub/set screw with a light dab of thread lock to ensure it doesn't work loose.

original lock components and new brass ones

The motor/gearbox assembly is mounted using two M1.6 countersunk machine screws to a short length of flat brass bar, which is secured to the base of the padlock using a single M3 pan head screw and nylon washer. This screw is secured into the lock body via a tapped hole with thread lock.

The hole in the flat bar, through which the pan screw passes, is drilled to a diameter of 3mm. This diameter, combined with the screw not being overly tightened, allows for some degree of lateral movement relative to the base of the padlock - this free movement is sufficient to prevent any binding of the brass cylinder when being rotated by the motor.

lime lock display, counting options time lock display, random timer options time lock display, main menu time lock display, flashing options time lock display, auto off options time lock display, unlocking attempts options time lock display, battery status time lock display, counting down time lock display, powering off

The menu system's navigated using the left and right arrow keys on the remote, the up and down keys select the menu option in the first six screens. Values are entered by pressing the numeric buttons, and on each press the cursor's moved to the next position ready for the next value. When the final value has been entered the cursor returns to the first position.

All the values entered in the menu system are saved to EEPROM when the device shuts down, either by selecting POWER OFF or when the device powers off automatically. These values are reloaded when powering up, but can be restored to their defaults by selecting DEFAULTS from the main menu.

The device will also display an UNLOCKING message during the unlock process, and LOW BATTERY if such a condition's detected, after which it will immediately unlock.


/* 
 * File:   pic_random_time_lock_device.c
 * Author: paul reynolds ( paul at v9tronics d0t c0m )
 *
 * dual time-lock motor control board with countdown timer
 * using the NOKIA 5110 LCD display with an accurate clock
 * provided by an external 32.768KHz crystal driving TIMER1
 * 
 * IR remote control will drive the functions and allow 
 * operating parameters to be specified.
 * 
 * CREATED: 25th August 2022
 *
 * this was one of those projects where I spent so much effort 
 * getting the hardware right that once the software was working
 * I lost all interest in the project, and never looked at it 
 * again.
 * 
 * the code for the TimeTrap servo lock controller is far neater, 
 * and driving the 1602 LCD is less involved than the Nokia LCD 
 * which involves repositioning the cursor each time prior to 
 * writing the text.
 */


// PIC18F26K22 Configuration Bit Settings

// 'C' source line config statements

// CONFIG1H
#pragma config FOSC = HSMP      // Oscillator Selection bits (HS oscillator (medium power 4-16 MHz))
#pragma config PLLCFG = ON      // 4X PLL Enable (Oscillator multiplied by 4)
#pragma config PRICLKEN = ON    // Primary clock enable bit (Primary clock enabled)
#pragma config FCMEN = OFF      // Fail-Safe Clock Monitor Enable bit (Fail-Safe Clock Monitor disabled)
#pragma config IESO = OFF       // Internal/External Oscillator Switchover bit (Oscillator Switchover mode disabled)

// CONFIG2L
#pragma config PWRTEN = OFF     // Power-up Timer Enable bit (Power up timer disabled)
#pragma config BOREN = OFF      // Brown-out Reset Enable bits (Brown-out Reset disabled in hardware and software)
#pragma config BORV = 190       // Brown Out Reset Voltage bits (VBOR set to 1.90 V nominal)

// CONFIG2H
#pragma config WDTEN = OFF      // Watchdog Timer Enable bits (Watch dog timer is always disabled. SWDTEN has no effect.)
#pragma config WDTPS = 32768    // Watchdog Timer Postscale Select bits (1:32768)

// CONFIG3H
#pragma config CCP2MX = PORTB3  // CCP2 MUX bit (CCP2 input/output is multiplexed with RB3)
#pragma config PBADEN = OFF     // PORTB A/D Enable bit (PORTB<5:0> pins are configured as digital I/O on Reset)
#pragma config CCP3MX = PORTB5  // P3A/CCP3 Mux bit (P3A/CCP3 input/output is multiplexed with RB5)
#pragma config HFOFST = ON      // HFINTOSC Fast Start-up (HFINTOSC output and ready status are not delayed by the oscillator stable status)

#pragma config T3CMX = PORTC0   // Timer3 Clock input mux bit (T3CKI is on RC0)
#pragma config P2BMX = PORTB5   // ECCP2 B output mux bit (P2B is on RB5)
#pragma config MCLRE = EXTMCLR  //EXT MCLR Pin Enable bit (MCLR pin enabled, RE3 input pin disabled)

// CONFIG4L
#pragma config STVREN = OFF     // ON// Stack Full/Underflow Reset Enable bit (Stack full/underflow will cause Reset)
#pragma config LVP = ON         // Single-Supply ICSP Enable bit (Single-Supply ICSP enabled if MCLRE is also 1)
#pragma config XINST = OFF      // Extended Instruction Set Enable bit (Instruction set extension and Indexed Addressing mode disabled (Legacy mode))

// CONFIG5L
#pragma config CP0 = OFF        // Code Protection Block 0 (Block 0 (000800-003FFFh) not code-protected)
#pragma config CP1 = OFF        // Code Protection Block 1 (Block 1 (004000-007FFFh) not code-protected)
#pragma config CP2 = OFF        // Code Protection Block 2 (Block 2 (008000-00BFFFh) not code-protected)
#pragma config CP3 = OFF        // Code Protection Block 3 (Block 3 (00C000-00FFFFh) not code-protected)

// CONFIG5H
#pragma config CPB = OFF        // Boot Block Code Protection bit (Boot block (000000-0007FFh) not code-protected)
#pragma config CPD = OFF        // Data EEPROM Code Protection bit (Data EEPROM not code-protected)

// CONFIG6L
#pragma config WRT0 = OFF       // Write Protection Block 0 (Block 0 (000800-003FFFh) not write-protected)
#pragma config WRT1 = OFF       // Write Protection Block 1 (Block 1 (004000-007FFFh) not write-protected)
#pragma config WRT2 = OFF       // Write Protection Block 2 (Block 2 (008000-00BFFFh) not write-protected)
#pragma config WRT3 = OFF       // Write Protection Block 3 (Block 3 (00C000-00FFFFh) not write-protected)

// CONFIG6H
#pragma config WRTC = OFF       // Configuration Register Write Protection bit (Configuration registers (300000-3000FFh) not write-protected)
#pragma config WRTB = OFF       // Boot Block Write Protection bit (Boot Block (000000-0007FFh) not write-protected)
#pragma config WRTD = OFF       // Data EEPROM Write Protection bit (Data EEPROM not write-protected)

// CONFIG7L
#pragma config EBTR0 = OFF      // Table Read Protection Block 0 (Block 0 (000800-003FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR1 = OFF      // Table Read Protection Block 1 (Block 1 (004000-007FFFh) not protected from table reads executed in other blocks)
#pragma config EBTR2 = OFF      // Table Read Protection Block 2 (Block 2 (008000-00BFFFh) not protected from table reads executed in other blocks)
#pragma config EBTR3 = OFF      // Table Read Protection Block 3 (Block 3 (00C000-00FFFFh) not protected from table reads executed in other blocks)

// CONFIG7H
#pragma config EBTRB = OFF      // Boot Block Table Read Protection bit (Boot Block (000000-0007FFh) not protected from table reads executed in other blocks)

// #pragma config statements should precede project file includes.
// Use project enums instead of #define for ON and OFF.


#define _XTAL_FREQ 32000000

#include <xc.h>

#include <pic18f26k22.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdint.h>



//
// define pin connections
//

#define MOTOR_A_FW  PORTBbits.RB2
#define MOTOR_A_BK  PORTBbits.RB4

#define MOTOR_B_FW  PORTBbits.RB3
#define MOTOR_B_BK  PORTBbits.RB5

#define BACK_LIGHT  PORTBbits.RB1
#define L293_OFF    PORTCbits.RC4
#define POWER_OFF   PORTAbits.RA2


//
// define some states - an ease of readability thing
//

#define ON          1
#define OFF         0
#define START       1
#define STOP        0
#define TRUE        1
#define FALSE       0

#define MOTOR_A     0
#define MOTOR_B     1


//
// menu screen names and their numerical position
//

#define DISPLAY_COUNTDOWN   1
#define DISPLAY_RANDOM      2
#define DISPLAY_SYSTEM      3
#define DISPLAY_FLASH       4
#define DISPLAY_AUTO_OFF    5
#define DISPLAY_UNLOCKING   6
#define DISPLAY_BATTERY     7
#define DISPLAY_MANUAL      8

//
// control structure variables
//

char serial_text[12];

unsigned int adc_value = 0;

unsigned int TIMER_COUNTDOWN = FALSE;

unsigned int LCD_DISPLAY     = ON;

unsigned int REPEAT_UNLOCK   = FALSE;

unsigned int RELEASE_LOCKS   = FALSE;


//
// use button presses to make random number more random
//

unsigned int press_count_rnd = 0;


//
// infra-red remote decoding variables
//

unsigned int ir_pos  = 0;

unsigned int ir_dat[67];

unsigned int ir_cnt  = 0;

unsigned int ir_decode = 0;


//
// for manipulating display text
//

char str[12];


//
// clock and timer variables
//

int t_seconds     = 0;
int t_minutes     = 0;
int t_hours       = 0;
   
int t_max_hours   = 0;
int t_max_minutes = 0;
int t_min_hours   = 0;
int t_min_minutes = 0;

int rnd_minutes   = 0;
int min_minutes   = 0;
int max_minutes   = 0;

int clock_ticker  = 0;

unsigned int r_min[4] = {0,0,1,0};
unsigned int r_max[4] = {0,1,0,0};
unsigned int c_tim[4] = {0,0,2,0};


//
// flasher variables
//

unsigned int flash_ticker    = 0;   // timer incremented counter

unsigned int flash_interval  = 30;  // flash the display back light every 30 seconds
            
unsigned int flash_duration  = 1;   // keep the display back light on for 1 second

int f_int[3] = {0,3,0};
int f_dur[3] = {0,1};


//
// auto power off variables
//

unsigned int auto_pwr_off   = 0;     // timer incremented counter

unsigned int shutdown_after = 60;   // shutdown after duration

int s_dur[3] = {0,6,0};


//
// repeat unlocking variables
//

unsigned int unlock_again    = 0;    // timer incremented counter

unsigned int unlock_count    = 0;    // number of unlock attempts

unsigned int unlock_attempts = 3;    // attempts at unlocking

unsigned int unlock_interval = 10;    // seconds between attempts

int u_int[2] = {3,0};



//
// battery status variables
//

int power_test   = 0;     // timer incremented counter

//float batt_voltage          = 0.00;  // the battery voltage

//float warn_voltage          = 4.95;  // the voltage to show low battery warning

int   power_status          = 0;     // battery capacity as a percentage

//int w_vol[3] = {4,9,5};



//
// nokia 5110 display variables
//

int load_screen = 0;    // trigger a screen refresh

int sel_screen  = 1;    // select associated menu screen - first screen by default

int sel_display = 0;    // select display mode b/l on, b/l off, off, on

int sel_line    = 0;    // the line currently selected - first line by default

int sel_lines   = 3;    // lines available in a menu - three is the maximum

int dig1, dig2, dig3, dig4;

//
// include for addressing the LCD display
//

#include "pic_nokia5110.h"





//////////////////////////////////
//// VARIOUS SYSTEM FUNCTIONS ////
////////////////////////////////// 

    void tx_string( char * data_arr ){


        unsigned int posi = 0;

        unsigned int leng = strlen( data_arr );

        while( posi < leng ){

            while(!TXSTAbits.TRMT);

            TXREG1 = data_arr[posi];

            while(!TXSTAbits.TRMT);

            __delay_ms(2);

            posi ++;
        }

    }
    

    void flash_backlight( int speed, int times ){
  
        for( int i=1; i<=times; i++ ){
            
            BACK_LIGHT = ON;
            
            for( int y=0; y<=speed; y++ ){ __delay_ms(10); }                    
            
            BACK_LIGHT = OFF;
            
            for( int y=0; y<=speed; y++ ){ __delay_ms(10); }
        }

    }
    
       
    void eeprom_w( unsigned int address, int data ){
    
        while( EECON1bits.WR ); // wait for any previous write to complete

        EEADR   = address;        
        EEDATA  = data;

        EECON1bits.EEPGD = 0;  // clear to point EEPROM memory        
        EECON1bits.CFGS  = 0;  // clear to access EEPROM
        EECON1bits.WREN  = 1;  // enable write operation
        
        INTCONbits.GIE   = 0;  // disable interrupts while writing
        EECON2 = 0x55;
        EECON2 = 0xAA;
        EECON1bits.WR   = 1;   // set to begin write
        INTCONbits.GIE  = 1;   // enable interrupts
        EECON1bits.WREN = 0;   // disable write operation
        EECON1bits.WR   = 0;   // clear in preparation for next write
        
        // this is just for debugging - remove later
        if( EECON1bits.WRERR ){
        
            EECON1bits.WRERR = 0;
            
            // these don't get cleared if an error occurs, so clear now
            EECON1bits.EEPGD = 0;
            EECON1bits.CFGS  = 0;
        }
    }
    
    
    int eeprom_r( unsigned int address ){
        
        EEADR = address;
        EECON1bits.EEPGD = 0;  // clear to point to the EEPROM
        EECON1bits.CFGS  = 0;  // clear to access eeprom
        EECON1bits.RD    = 1;  // enable read operation
        
        int data = EEDATA;
        
        EECON1bits.RD    = 0;  // clear in preparation for next read
        return data;        
    }
    

    // get the battery voltage
    /*
    float get_battery_voltage(){

        ADCON0bits.CHS  = 1;  // AN1/RA1 - our voltage divider

        ADCON0bits.ADON = ON;
        ADCON0bits.GO   = 1;                  // start the conversion        
        while( ADCON0bits.GO == 1 ){ ; };   // wait for the conversion to end
        float adc_v = (ADRESH<<8)+ADRESL;	// combine the 10 bits of the conversion
        ADCON0bits.ADON = OFF;         

        float v;
                
        v = 3.32 * ( adc_v / 1023 ) * 2;
        
        return v;
    }
    */
    
    // get the battery status
    int get_battery_status(){

        ADCON0bits.CHS  = 1;  // AN1/RA1 - voltage divider

        ADCON0bits.ADON = ON;
        ADCON0bits.GO   = 1;                // start the conversion        
        while( ADCON0bits.GO == 1 ){ ; };   // wait for the conversion to end
        float adc_v = (ADRESH<<8)+ADRESL;	// combine the 10 bits of the conversion
        ADCON0bits.ADON = OFF;         

        float v;
                
        v = 3.32 * ( adc_v / 1023 ) * 2;

        float v_minimum = 5.00;
        
        float v_nominal = 6.00;

        int pc_remaining = ( (v - v_minimum) / (v_nominal - v_minimum) ) * 100;

        if( pc_remaining > 100 ){ pc_remaining = 100; }

        if( pc_remaining < 0   ){ pc_remaining = 0;   }
        
        return pc_remaining;
    }
    
    
    
    // get a random integer between a min and max
    int random_between(int min, int max){
        
        // prevent div/0 errors - as user input is unknown
        if(min <= 0){ min = 1; }
        if(max <= 0){ max = 1; }
        
        if(min == max){ return 1; }
    
        srand( power_test * press_count_rnd );
        
       return min + rand() % (max - min);
    }    

    
    // get total minutes from hours plus minutes
    int minutes_from_h_and_m( int hours, int minutes ){

        unsigned int t_minutes = hours * 60;

        t_minutes = t_minutes + minutes;

        return t_minutes;
    }

    
    // get whole hours from an integer of minutes
    int h_from_minutes( int minutes ){
        
        if( minutes >= 60 ){    
            t_hours = minutes / 60;
        }else{
            t_hours = 0;
        }
        return t_hours;
    }

    
    // get minutes remainder - having already deducted whole hours above
    int m_from_minutes( int minutes ){
        
        if( minutes > 0){
            t_minutes = minutes % 60;
        }else{
            t_minutes = 0;
        }
        
        return t_minutes;
    }
    
    
    // hold up until motor has stalled, and the ADC reaches a consistent value
    void do_until_stalled(){

        ADCON0bits.CHS  = 0;
        ADCON0bits.ADON = ON;        
        
        __delay_ms(30);
                    
        int values[10] = {0,0,0,0,0,0,0,0,0,0};
                
        int total,avg_total,x;
        
        int y = 0;

            sprintf( serial_text, "\r\n __START__\r\n" );
            tx_string( serial_text );        
        
        while(1){

            ADCON0bits.GO   = 1;

            while( ADCON0bits.GO == 1 ){ ; };

            int adc_value = (ADRESH<<8)+ADRESL;
            
            __delay_ms(30);
            
            sprintf( serial_text, " %d\r\n", adc_value );
            tx_string( serial_text ); 
            
            // store the most recent ten ADC values
            values[y] = adc_value;  y++;
            if( y == 5 ){ y = 0; }      

            // total up the last five ADC values stored
            total=0;  x=0;
            while( x < 5 ){ total = total + values[x];  x++; }
            
            avg_total = total / 5;
            
            sprintf( serial_text, " TOTAL/5: %d\r\n", avg_total );
            tx_string( serial_text );
            
            // if the total divided by ten matches the most recent value, we're done!
            if( total / 5 >= adc_value ){ break; }
        }
        
        ADCON0bits.ADON = OFF;
        
        // extra delay - just to drive the motors that bit further
        __delay_ms(300);
    }
    
    
    void release_lock( int motor ){        
        
        INTCONbits.GIE = 0; // disable interrupts while unlocking
        
        /*
        for( int i=1; i<=4; i++ ){
            BUZZER = ON;
            __delay_ms(200);
            BUZZER = OFF;
            __delay_ms(200);
        }
        */
        
        L293_OFF = FALSE;   // PNP MOSFET on RC7, so must go LOW to turn ON
        
        __delay_ms(500);
        
            //////////////////////////
            // RUN THE MOTOR FORWARD
            //////////////////////////
            
            if( motor == MOTOR_A ){ MOTOR_A_FW = TRUE; }
            if( motor == MOTOR_B ){ MOTOR_B_FW = TRUE; }
            
            do_until_stalled();
            
            if( motor == MOTOR_A ){ MOTOR_A_FW = FALSE; }
            if( motor == MOTOR_B ){ MOTOR_B_FW = FALSE; }
            
            adc_value = 0;
            
            __delay_ms(300);
            
            
            ////////////////////////////
            // RUN THE MOTOR BACKWARDS
            ////////////////////////////            

            if( motor == MOTOR_A ){ MOTOR_A_BK = TRUE; }            
            if( motor == MOTOR_B ){ MOTOR_B_BK = TRUE; }
            
            do_until_stalled();
            
            if( motor == MOTOR_A ){ MOTOR_A_BK = FALSE; }
            if( motor == MOTOR_B ){ MOTOR_B_BK = FALSE; }            
            
            adc_value = 0;
            
            __delay_ms(300);            
            
            
            //////////////////////////////////////////////////////
            // run the motor forward slightly to prevent jamming
            //////////////////////////////////////////////////////

            if( motor == MOTOR_A ){ MOTOR_A_FW = TRUE; }
            if( motor == MOTOR_B ){ MOTOR_B_FW = TRUE; }
            
            __delay_ms(150);            
            
            if( motor == MOTOR_A ){ MOTOR_A_FW = FALSE; }
            if( motor == MOTOR_B ){ MOTOR_B_FW = FALSE; }
            
            adc_value = 0;
            
            __delay_ms(500);
            
            L293_OFF = TRUE;    // HI to turn OFF
            
            INTCONbits.GIE = 1; // enable interrupts
    }

    
    
    
///////////////////////////////////
//// NOKIA LCD DISPLAY SCREENS ////
///////////////////////////////////    
    
    void display_begin( int text_size ){
        
        LCD_Clear();
        LCD_Display();
        LCD_TextSize( text_size );
        LCD_TextColor(BLACK, WHITE);
    }

    
    void display_mode_random( int sel_line ){
    
        /*
             RANDOM
         ---------------
           MAX 01:00
           MIN 00:05
            [START]        
        */
        
        sel_lines = 3;
        
        display_begin(1);

        LCD_GotoXY(24, 2);
        LCD_Print("RANDOM");
        LCD_GotoXY(0, 9);
        LCD_Print("--------------");
        
        (sel_line==1) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(15, 17);
        
        dig1=r_min[0]; dig2=r_min[1]; dig3=r_min[2]; dig4=r_min[3];
        
        sprintf( str, "MIN %i%i:%i%i", dig1, dig2, dig3, dig4 );        
        LCD_Print( str );
        
        (sel_line==2) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(15, 27);
        
        dig1=r_max[0]; dig2=r_max[1]; dig3=r_max[2]; dig4=r_max[3];        
        
        sprintf( str, "MAX %i%i:%i%i", dig1, dig2, dig3, dig4 );        
        LCD_Print( str );
        
        (sel_line==3) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(21, 38);
        LCD_Print("[START]");        

        LCD_Display();
        LCD_TextColor(BLACK, WHITE);
    }

    
    void display_mode_countdown( int sel_line ){
    
        /*
            COUNTDOWN
         ---------------
           TIME 01:00

            [START]
        */
        
        sel_lines = 2;        
                
        display_begin(1);

        LCD_GotoXY(15, 2);
        LCD_Print("COUNTDOWN");
        LCD_GotoXY(0, 9);
        LCD_Print("--------------");
        
        (sel_line==1) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(12, 17);
        
        dig1=c_tim[0]; dig2=c_tim[1]; dig3=c_tim[2]; dig4=c_tim[3];         
        
        sprintf( str, "TIME %i%i:%i%i",  dig1, dig2, dig3, dig4 );        
        LCD_Print( str );
        
        (sel_line==2) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(21, 38);
        LCD_Print("[START]");        

        LCD_Display();     
        LCD_TextColor(BLACK, WHITE);
    }
    
    
    void display_mode_unlock(){
            
        display_begin(2);

        LCD_GotoXY(7, 16);
        LCD_Print("UNLOCK");
        LCD_Display();    
    }
    
    
    void display_mode_goodbye(){

        // always save user data here prior to powering off

        eeprom_w( 0x11, r_min[0] );
        eeprom_w( 0x12, r_min[1] );
        eeprom_w( 0x13, r_min[2] );
        eeprom_w( 0x14, r_min[3] );

        eeprom_w( 0x21, r_max[0] );
        eeprom_w( 0x22, r_max[1] );
        eeprom_w( 0x23, r_max[2] );
        eeprom_w( 0x24, r_max[3] );

        eeprom_w( 0x31, c_tim[0] );
        eeprom_w( 0x32, c_tim[1] );
        eeprom_w( 0x33, c_tim[2] );
        eeprom_w( 0x34, c_tim[3] );
        
        eeprom_w( 0x41, flash_interval );
        eeprom_w( 0x42, f_int[0] );
        eeprom_w( 0x43, f_int[1] );
        eeprom_w( 0x44, f_int[2] );
        
        eeprom_w( 0x45, flash_duration );
        eeprom_w( 0x46, f_dur[0] );
        eeprom_w( 0x47, f_dur[1] );
        
        eeprom_w( 0x49, shutdown_after );
        eeprom_w( 0x4A, s_dur[0] );
        eeprom_w( 0x4B, s_dur[1] );
        eeprom_w( 0x4C, s_dur[2] );
        
        eeprom_w( 0x4D, unlock_attempts );
        eeprom_w( 0x4E, u_int[0] );
        eeprom_w( 0x4F, u_int[1] );
                
        eeprom_w( 0x05, 5 );  // set so we read from EEPROM on startup
        
        display_begin(2);
                
        LCD_GotoXY(1, 16);
        LCD_Print("GOODBYE");
        LCD_Display();    
        
        __delay_ms(2000);
        
        BACK_LIGHT = OFF;
        
        display_begin(2);
        
        __delay_ms(1000);        
        
        POWER_OFF = TRUE;        
    }    
    
    
    void display_mode_battery_low(){
            
        display_begin(2);
        
        LCD_GotoXY(1, 8);
        LCD_Print("BATTERY");
        LCD_GotoXY(25, 27);
        LCD_Print("LOW");
        LCD_Display();    
    }    

    
    void display_menu_system( int sel_line ){
    
        /*
           MAIN MENU
         ---------------
           POWER OFF
           UNLOCK
           DEFAULTS
        */
        
        sel_lines = 3;        
        
        display_begin(1);
        
        LCD_GotoXY(15, 2);
        LCD_Print("MAIN MENU");
        LCD_GotoXY(0, 9);
        LCD_Print("--------------");
        
        (sel_line==1) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);
        LCD_GotoXY(14, 17);
        LCD_Print("POWER OFF");
        
        (sel_line==2) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);
        LCD_GotoXY(14, 27);
        LCD_Print("UNLOCK");
        
        (sel_line==3) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(14, 37);
        LCD_Print("DEFAULTS");
        
        LCD_Display();  
        LCD_TextColor(BLACK, WHITE);
    }

    
    void display_menu_flash( int sel_line ){
        
        /*
          FLASH DISPLAY
         ---------------
           INTERVAL 60
           DURATION 1
        */
        
        sel_lines = 2;        
        
        display_begin(1);
        
        LCD_GotoXY(4, 2);
        LCD_Print("FLASH DISPLAY");
        LCD_GotoXY(0, 9);
        LCD_Print("--------------");
        
        (sel_line==1) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(6, 17);
        
        dig1=f_int[0]; dig2=f_int[1]; dig3=f_int[2];
         
        sprintf( str, "INTERVAL %i%i%i", dig1, dig2, dig3 );
        LCD_Print( str );        
        
                
        (sel_line==2) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(6, 27);
        
        dig1=f_dur[0]; dig2=f_dur[1];
         
        sprintf( str, "DURATION %i%i", dig1, dig2 );
        LCD_Print( str );        

        LCD_Display();     
        LCD_TextColor(BLACK, WHITE);
    }    
     
    
    void display_menu_unlocking( int sel_line ){
        
        /*
              UNLOCKING
            ---------------
              ATTEMPTS 1
              INTERVAL 5
        */
        
        sel_lines = 2;
        
        display_begin(1);
        
        LCD_GotoXY(15, 2);
        LCD_Print("UNLOCKING");
        LCD_GotoXY(0, 9);
        LCD_Print("--------------");
        
        (sel_line==1) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(9, 17);
                
        sprintf( str, "ATTEMPTS %i", unlock_attempts );
        LCD_Print( str );             

        (sel_line==2) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(9, 27);
        
        dig1=u_int[0]; dig2=u_int[1];
         
        sprintf( str, "INTERVAL %i%i", dig1, dig2 );
        LCD_Print( str );  
        
        LCD_Display();     
        LCD_TextColor(BLACK, WHITE);
    }
    
    
    void display_menu_auto_off( int sel_line ){
        
        /*
            AUTO OFF
         --------------
           AFTER 240
        */
        
        sel_lines = 1;        
        
        display_begin(1);
        
        LCD_GotoXY(18, 2);
        LCD_Print("AUTO OFF");
        LCD_GotoXY(0, 9);
        LCD_Print("--------------");
        
        (sel_line==1) ? LCD_TextColor(WHITE,BLACK) : LCD_TextColor(BLACK, WHITE);        
        LCD_GotoXY(15, 17);
        
        dig1=s_dur[0]; dig2=s_dur[1]; dig3=s_dur[2];
         
        sprintf( str, "AFTER %i%i%i", dig1, dig2, dig3 );
        LCD_Print( str );             

        LCD_Display();     
        LCD_TextColor(BLACK, WHITE);
    }    
    

    void display_menu_battery( int show_version ){
        
        /*
                BATTERY
            ---------------
                 100%
        */
                        
        display_begin(1);
        
        LCD_GotoXY(22, 2);
        LCD_Print("BATTERY");
        LCD_GotoXY(0, 9);
        LCD_Print("--------------");
                
        power_status = get_battery_status();
        
        LCD_TextSize(2);
        
        if( power_status == 100 ){
            LCD_GotoXY(18, 19);
        }else{
            LCD_GotoXY(25, 19);            
        }

        if( power_status == 100 ){  LCD_GotoXY(18, 19);  } // xxx%
        if( power_status < 100  ){  LCD_GotoXY(25, 19);  } // xx%
        if( power_status < 10   ){  LCD_GotoXY(22, 19);  } // x% 
        
        sprintf( str, "%d%%", power_status );
        LCD_Print( str );

        
        if( show_version ){
            
            LCD_TextSize(1);
            LCD_GotoXY(24, 38);
            sprintf( str, "V2232A" );
            LCD_Print( str );
        }
        
        LCD_Display();     
        LCD_TextColor(BLACK, WHITE);
    }
    
    
    
 
////////////////////////////////////////
//// PROCESS IR REMOTE CONTROL DATA ////
////////////////////////////////////////
            
    void get_ir_data(){

        ir_cnt = 0;
        ir_pos = 0;

        int ir_pin_val_old;
        int ir_pin_val;           

        ir_pin_val_old = PORTBbits.RB0;

        while(1){

            ir_pin_val = PORTBbits.RB0;

            if( ir_pin_val == ir_pin_val_old ){

                ir_cnt ++; 

            }else{

                ir_dat[ir_pos] = ir_cnt;

                ir_pos ++;                    

                ir_pin_val_old = ir_pin_val;

                ir_cnt = 0;                    
            }

            if( ( PORTBbits.RB0 == 1 && ir_cnt > 1000 ) ){ break; }
            
        }

        ir_decode = TRUE; 
        

    }


    
    
///////////////////////////
//// INTERRUPT HANDLER ////
///////////////////////////     
    
    void __interrupt() isr (void){

        // TIMER1 - the 32.768kHz crystal gives up our clock pulse
        if( TMR1IF ){ 
            
            // clear interrupt flag
            TMR1IF = 0;                   

            // increment battery test counter
            power_test ++;
            if( power_test > 100 ){ power_test = 0; }           
            
            // increment auto power off counter
            auto_pwr_off ++;
            if( auto_pwr_off > 300 ){ auto_pwr_off = 0; }
            
            // increment unlock again counter
            unlock_again ++;
            if( unlock_again > 100 ){ unlock_again = 0; }
            
            
            // resetting the timer/counter to this value and counting up until 
            // an overflow occurs is what gives us an accurate 1sec interrupt
            // that sets the clock_ticker and drives the clock routine below
            TMR1H    = 0x80;
            TMR1L    = 0x00;
            
            clock_ticker = 1;
        }         
        
        
        // INT0 - infrared sensor
        if( INTCONbits.INT0IF ){ 
            
            INTCONbits.INT0IE = 0;            
            
            get_ir_data();
        }            
    }

    


    
////////////////////////////////////////////////////
//// HERE IS WERE WE START ACTUALLY DOING STUFF ////
////////////////////////////////////////////////////
    
int main(){
    
    OSCCONbits.IDLEN    = 0;
    OSCCONbits.OSTS     = 1;
    OSCCONbits.SCS      = 0;

    OSCCON2bits.PLLRDY  = 1;
    OSCCON2bits.SOSCRUN = 1;
    OSCCON2bits.SOSCGO  = 1;
            
    TRISA  = 0b00000011;    // all RA pins output, except RA0/AN0, RA1/AN1
    TRISB  = 0b00000001;    // all RB pins output, except RB0/INT0   
    TRISC  = 0b00000011;    // all RC pins output, except RC0, RC1 used by 32.768 crystal

    ANSELA = 0b00000011;    
    ANSELB = 0b00000000;    
    ANSELC = 0b00000000;
    
    // port slew rate control - standard transition
    SLRCONbits.SLRA = 0;
    SLRCONbits.SLRB = 0;
    SLRCONbits.SLRC = 0;

    SSP1CON1bits.SSPEN = 0;
    
    // TIMER1 - 1sec clock tick
    T1CONbits.RD16      = 0;
    T1CONbits.T1CKPS    = 0;
    T1CONbits.T1SOSCEN  = 1;
    T1CONbits.T1SYNC    = 1;
    T1CONbits.TMR1CS    = 1;
    T1CONbits.TMR1ON    = 1;  
    T1CONbits.TMR1CS0   = 0;
    T1CONbits.TMR1CS1   = 1;
    T1GCONbits.TMR1GE   = 0;    // gate enable - counts regardless of gate function
    PIE1bits.TMR1IE     = 1;    // timer 1 interrupt enable

    
    // ADC - measure motor current and battery voltage
    ADCON2bits.ADFM     = 1;       // ADC result is right justified
    ADCON2bits.ADCS     = 0b010;   // Fosc/16 is the conversion clock  
    ADCON2bits.ACQT     = 0b110;   // acquisition time 
        
    ADCON1bits.TRIGSEL  = 0b0;
    ADCON1bits.PVCFG    = 0b00;    // V REF + source = VDD/+5V
    ADCON1bits.NVCFG    = 0b00;    // V REF - source = VSS/GND    
    
    
    // 8-bit asynchronous serial
    TXSTA1bits.CSRC     = 1;   // clock from BRG
    TXSTA1bits.SYNC     = 0;   // asynchronous EUART
    TXSTA1bits.TXEN     = 1;   // enables transmitter
    TXSTA1bits.TX9      = 0;   // 8bit transmit
    
    RCSTA1bits.CREN     = 0;   // enables receiver - disabled    
    RCSTA1bits.RX9      = 0;   // set to 8bit receive
    RCSTA1bits.SPEN     = 1;   // serial port enable
    
    TXSTA1bits.BRGH     = 0;   // baud rate generator - high baud rate
    TXSTA1bits.SENDB    = 1;   // send sync break on next transmission        
    BAUDCON1bits.BRG16  = 0;   // baud rate generator - 8 bit operation
    BAUDCON1bits.CKTXP  = 0;   // tx polarity
    SPBRG1              = 51;  // baud rate generator - 51=9600, 103=4800, 207=2400

    // interrupts
    //RCIE   = 0;  // receive interrupt enable PG.253
    GIE     = 1;   // global interrupt enable
    PEIE    = 1;   // peripheral interrupt enable bit       
    INT0IE  = 1;   // enable INT0
    INTEDG0 = 1;   // rising edge
    
    
    BACK_LIGHT = OFF;
    
    MOTOR_A_FW = FALSE;

    MOTOR_A_BK = FALSE;    
    
    MOTOR_B_FW = FALSE;

    MOTOR_B_BK = FALSE;
    
    L293_OFF   = TRUE;     // PNP MOSFET so 1=OFF, 0=ON
    
    POWER_OFF  = FALSE;
    

    
    // set main control structure variables
    
    TIMER_COUNTDOWN = FALSE;
    
    RELEASE_LOCKS   = FALSE;
    
    LCD_DISPLAY     = ON;    
    
    REPEAT_UNLOCK   = FALSE;
    
    
    // initialise NOKIA 5110 display - show splash screen etc.
        
    BACK_LIGHT = ON;
    
    __delay_ms(200);
    
    
    LCD_Begin();
    
    LCD_SetContrast(75);
    
    display_menu_battery( TRUE );
    
    __delay_ms(3500);
    
    BACK_LIGHT  = OFF;
    
    // now show the countdown timer screen
    
    sel_screen = 1; 
    
    sel_line = 1;
    
    load_screen = TRUE;

    
    //
    // remote control decoding
    //
    
    unsigned long nec_data; 

    unsigned long nec_temp;
    
    unsigned long nec_reversed;

    int z = 0;
    int y = 0;
    
    int b1, b2, b3, b4;
    
    
    //
    // remote control button handling
    //
    
    int ir_command = 0; 
    
    int numb_press = 0;
    
    int digit_pos  = 0;
    
    
    //
    // restore user data if we've previously written to the EEPROM
    //

    if( eeprom_r( 0x05 ) == 5 ){
        
        r_min[0] = eeprom_r( 0x11 );
        r_min[1] = eeprom_r( 0x12 );
        r_min[2] = eeprom_r( 0x13 );
        r_min[3] = eeprom_r( 0x14 );

        r_max[0] = eeprom_r( 0x21 );
        r_max[1] = eeprom_r( 0x22 );
        r_max[2] = eeprom_r( 0x23 );
        r_max[3] = eeprom_r( 0x24 );

        c_tim[0] = eeprom_r( 0x31 );
        c_tim[1] = eeprom_r( 0x32 );
        c_tim[2] = eeprom_r( 0x33 );
        c_tim[3] = eeprom_r( 0x34 );
        
        flash_interval = eeprom_r( 0x41 );
        f_int[0] = eeprom_r( 0x42 );
        f_int[1] = eeprom_r( 0x43 );
        f_int[2] = eeprom_r( 0x44 );
        
        flash_duration = eeprom_r( 0x45 );
        f_dur[0] = eeprom_r( 0x46 );
        f_dur[1] = eeprom_r( 0x47 );
        //f_dur[2] = eeprom_r( 0x48 );

        shutdown_after = eeprom_r( 0x49 );
        s_dur[0] = eeprom_r( 0x4A );
        s_dur[1] = eeprom_r( 0x4B );
        s_dur[2] = eeprom_r( 0x4C );
        
        unlock_attempts = eeprom_r( 0x4D );
        u_int[0] = eeprom_r( 0x4E );
        u_int[1] = eeprom_r( 0x4F );        
        
        //w_vol[0] = eeprom_r( 0x51 );
        //w_vol[1] = eeprom_r( 0x52 );
        //w_vol[2] = eeprom_r( 0x53 );
    }

    // restore the warn_voltage from the saved digits
    //warn_voltage = (float)w_vol[0];
    //warn_voltage = warn_voltage + (float)w_vol[1]/10;
    //warn_voltage = warn_voltage + (float)w_vol[2]/100; 


    // sprintf( serial_text, "FLA I:%d D:%d\r\n", flash_interval, flash_duration );
    // tx_string( serial_text );

    /*            
    sprintf( serial_text, "\r\n SHUTDOWN AFTER: %d\r\n", shutdown_after );
    tx_string( serial_text );

    sprintf( serial_text, "\r\n UNLOCK ATTEMPTS: %d\r\n", unlock_attempts );
    tx_string( serial_text ); 

    sprintf( serial_text, "\r\n WARN VOLTAGE: %4.2lf\r\n", warn_voltage );
    tx_string( serial_text );    
    */    
  
    

    while(1){      

    ////////////////////////////
    //// IR REMOTE DECODING ////
    ////////////////////////////
        
        // decode raw timing data from INT0 interrupt
        
        if( ir_decode ){
            
            z = 0;
            y = 0;
            
        // find the start by skipping everything that isn't our long pulse
            while(  z < 67  ){  

                if( (ir_dat[z] > 500) && (ir_dat[z] < 1200) ){

                    z++; // advance once
                    
                    nec_data = 0;

                    // convert timings to bits
                        while(  y < 64 ){

                            if( ir_dat[z] > 80 && ir_dat[z] < 180 ){

                                if( ir_dat[z+1] > 50 && ir_dat[z+1] < 200 ){
                                  nec_data = (nec_data << 1);  
                                }

                                if( ir_dat[z+1] > 280 && ir_dat[z+1] < 400 ){
                                  nec_data = (nec_data << 1);
                                  nec_data = (nec_data  | 1);                                    
                                }

                                z++; z++; // advance twice
                            }

                            y++;
                        } 

                    // reverse nec_data
                        for( int i=0; i<32; i++ ){

                            nec_temp     = ( nec_data >> i ) & 1;
                            nec_reversed = ( nec_reversed << 1 );

                            if( nec_temp ){
                              nec_reversed = ( nec_reversed | 1 );
                            }
                        }

                        nec_data = nec_reversed;
                        
                    // split up the 32 bits of nec_data into four bytes, invert b2 and b4
                        b1 =    (nec_data      ) & 0xFF;            // address
                        b2 = ~( (nec_data >> 8 ) & 0xFF ) & 0xFF;   // address inverted
                        b3 =    (nec_data >> 16) & 0xFF;            // command
                        b4 = ~( (nec_data >> 24) & 0xFF ) & 0xFF;   // command inverted

                    // validate and set ir_command
                        if( b1 == b2  &&  b3 == b4 ){
                            
                            ir_command = b3;
                            
                            b2 = b4 = 0;    // clear the inverted bytes
                        }

                    // reset 
                        nec_data = 0;
                }
               
                z++;                
            }

            
            // clear the data stored in ir_dat
            // this prevents unintentional double-presses and 
            // light flashes triggering the sensor and executing the last command
            
            for( int x = 0; x < 67; x++ ){ ir_dat[x] = 0; }
            
            
            // reset the auto power off timer - regard no input from the remote as inactivity!
            auto_pwr_off = 0;
            
            
            // count button presses - use this for seeding rand()
            press_count_rnd ++;
            
            if( press_count_rnd > 250 ){ press_count_rnd = 0; }
            
            
            // cancel any repeat unlock attempts - as device has been operated by remote
            REPEAT_UNLOCK = FALSE;
             
            unlock_count  = 0;

            
            __delay_ms(100);            

            
            // we're done processing the ir data
            // so enable INT0 interrupt, and clear INTO interrupt flag
            INTCONbits.INT0IE = 1;
            
            INTCONbits.INT0IF = 0;
            
            // clear so we can process any new ir data
            ir_decode = 0;            
        }
    
        
        

        
        
        
    ///////////////////////////////////////////////////
    //// TEST BATTERY PERIODICALLY - UNLOCK IF LOW ////
    ///////////////////////////////////////////////////
        
        if( TIMER_COUNTDOWN == TRUE && power_test > 15 ){
                
            power_test = 0;
            
            power_status = get_battery_status();
            
            if( power_status < 2 ){ 
            
                TIMER_COUNTDOWN = FALSE;
                
                display_mode_battery_low();

                __delay_ms(3500);
                
                display_mode_unlock();
                
                RELEASE_LOCKS = TRUE;
                
                unlock_count = 0;
            }

        }        
        
        
        
        
    ///////////////////////////////////////////////
    //// AUTO POWER OFF AFTER SPECIFIED PERIOD ////
    ///////////////////////////////////////////////

        if( auto_pwr_off > shutdown_after ){
            
            auto_pwr_off = 0;
                        
            if( TIMER_COUNTDOWN == FALSE ){
                       
                auto_pwr_off = 0;
                
                BACK_LIGHT = ON;

                display_mode_goodbye();
            }
        }
        
        
        
        
    ///////////////////////////////////////////////
    //// LOAD THE DISPLAY WHEN NAVIGATING MENU ////
    ///////////////////////////////////////////////
        
        if( load_screen ){
            
            load_screen = FALSE;
                        
            // reset digit position - after fourth digit return to the first
            if( digit_pos > 3 ){ digit_pos=0; }            

            // reset digit position - flash interval only uses three digits
            if( digit_pos > 2 && sel_screen == DISPLAY_FLASH && sel_line == 1 ){ digit_pos=0; }
            
            // reset digit position - flash duration only uses two digits
            if( digit_pos > 1 && sel_screen == DISPLAY_FLASH && sel_line == 2 ){ digit_pos=0; }            
            
            // reset digit position - auto off only uses three digits
            if( digit_pos > 2 && sel_screen == DISPLAY_AUTO_OFF && sel_line == 1 ){ digit_pos=0; }
            
            // reset digit position - unlock attempts menu only uses one digit
            if( digit_pos > 1 && sel_screen == DISPLAY_UNLOCKING && sel_line == 1 ){ digit_pos=0; }
            
            // reset digit position - unlock interval menu only uses two digits
            if( digit_pos > 2 && sel_screen == DISPLAY_UNLOCKING && sel_line == 2 ){ digit_pos=0; }
            
            // reset digit position - low battery menu only uses two digits
            //if( digit_pos > 2 && sel_screen == DISPLAY_BATTERY && sel_line == 1 ){ digit_pos=0; }

            switch( sel_screen ){
                case DISPLAY_COUNTDOWN:   display_mode_countdown( sel_line );
                break;
                case DISPLAY_RANDOM:      display_mode_random( sel_line );
                break;
                case DISPLAY_SYSTEM:      display_menu_system( sel_line );
                break;
                case DISPLAY_FLASH:       display_menu_flash( sel_line );
                break;
                case DISPLAY_UNLOCKING:   display_menu_unlocking( sel_line );
                break;
                case DISPLAY_AUTO_OFF:    display_menu_auto_off( sel_line );
                break;
                case DISPLAY_BATTERY:     display_menu_battery( FALSE );
                break;
            }
        }
        
        
        
        
    ///////////////////////////////////
    //// THE BASIC COUNTDOWN TIMER ////
    ///////////////////////////////////       
        
        // ENTER NEW COUNTDOWN TIMER VALUE
        if( sel_screen == DISPLAY_COUNTDOWN && sel_line == 1 && numb_press ){

            c_tim[digit_pos] = (numb_press=='z')?0:numb_press;
            
            if( c_tim[2] > 5 ){ c_tim[2] = 5; } // [xx:Mx] can't be greater than 5
            
            digit_pos ++;
            
            load_screen = TRUE;
            
            numb_press  = FALSE;            
        }

        // ON 'OK' PRESS - START COUNTDOWN TIMER 
        if( sel_screen == DISPLAY_COUNTDOWN && sel_line == 2 && ir_command == 0x1C ){
            
            ir_command = FALSE;
            
            if( TIMER_COUNTDOWN == FALSE ){
                
                BACK_LIGHT = OFF;

                t_hours = c_tim[0] * 10;
                t_hours = t_hours + c_tim[1];

                t_minutes = c_tim[2] * 10;
                t_minutes = t_minutes + c_tim[3];

                t_seconds = 0;            

                TIMER_COUNTDOWN = TRUE;                

                flash_ticker = 0;
                
                sel_display = 0;
                
                __delay_ms(250);
                
            }else{
                
                // pressing 'OK' cancels the timer if it's already running
                
                TIMER_COUNTDOWN = FALSE; 
                
                load_screen = TRUE; 
            }
        }
        
        
        
        
    ////////////////////////////////////
    //// THE RANDOM COUNTDOWN TIMER ////
    ////////////////////////////////////
        
        
        // ENTER NEW RANDOM TIMER 'MIN' VALUE
        if( sel_screen == DISPLAY_RANDOM && sel_line == 1 && numb_press ){
            
            r_min[digit_pos] = (numb_press=='z')?0:numb_press;
            
            if( r_min[2] > 5 ){ r_min[2] = 5; } // [xx:Mx] can't be greater than 5
            
            digit_pos ++;
            
            load_screen = TRUE;
            
            numb_press  = FALSE;
        }

        // ENTER NEW RANDOM TIMER 'MAX' VALUE        
        if( sel_screen == DISPLAY_RANDOM && sel_line == 2 && numb_press ){
                    
            r_max[digit_pos] = (numb_press=='z')?0:numb_press;
            
            if( r_max[2] > 5 ){ r_max[2] = 5; } // [xx:Mx] can't be greater than 5            
            
            digit_pos ++;
            
            load_screen = TRUE;
            
            numb_press  = FALSE;
        }
        
        // ON 'OK' PRESS START RANDOM TIMER  - GET RANDOM TIME FROM MIN AND MAX
        if( sel_screen == DISPLAY_RANDOM && sel_line == 3 && ir_command == 0x1C ){
            
            ir_command = FALSE;

            if( TIMER_COUNTDOWN == FALSE ){
                
                BACK_LIGHT = OFF;

                // convert array of time digits into variables we can work with
                
                // maximum time
                t_max_hours = r_max[0] * 10;
                t_max_hours = t_max_hours + r_max[1];

                t_max_minutes = r_max[2] * 10;
                t_max_minutes = t_max_minutes + r_max[3];

                // minimum time
                t_min_hours = r_min[0] * 10;
                t_min_hours = t_min_hours + r_min[1];

                t_min_minutes = r_min[2] * 10;
                t_min_minutes = t_min_minutes + r_min[3]; 
                
                // convert hours and minutes into just minutes
                max_minutes = minutes_from_h_and_m( t_max_hours, t_max_minutes );
                min_minutes = minutes_from_h_and_m( t_min_hours, t_min_minutes );
                
                // swap values if min_minutes is greater than max_minutes
                if( min_minutes > max_minutes ){ 
                    
                    min_minutes = minutes_from_h_and_m( t_max_hours, t_max_minutes );
                    max_minutes = minutes_from_h_and_m( t_min_hours, t_min_minutes );                
                }
                                
                // get a random number of minutes that's between min and max
                rnd_minutes = random_between( min_minutes, max_minutes );
                
                // convert random minutes to hours and minutes
                t_hours   = h_from_minutes( rnd_minutes );
                t_minutes = m_from_minutes( rnd_minutes );      
                t_seconds = 0;
                                
                TIMER_COUNTDOWN = TRUE;                

                flash_ticker = 0;                
                
                // clear and turn off the display
                // we want the random time to be unknown - unless the user chooses to display it
                LCD_DISPLAY = OFF; 
                
                LCD_Clear();
                
                LCD_Display();
                
                sel_display = 0;
                
                __delay_ms(250);
                
            }else{
            
                // pressing 'OK' cancels the timer if it's already running 
                
                TIMER_COUNTDOWN = FALSE; 
                
                LCD_DISPLAY = ON; 

                load_screen = TRUE;          
            }
        }





    /////////////////////////
    //// THE SYSTEM MENU ////
    /////////////////////////

        // ON 'OK' PRESS - SYSTEM MENU - POWER OFF
        if( sel_screen == DISPLAY_SYSTEM && sel_line == 1 && ir_command == 0x1C ){        
            
            ir_command = FALSE;
            
            BACK_LIGHT = ON;
            
            display_mode_goodbye();
        }
        
        // ON 'OK' PRESS - SYSTEM MENU - UNLOCK
        if( sel_screen == DISPLAY_SYSTEM && sel_line == 2 && ir_command == 0x1C ){        
            
            ir_command = FALSE;
                        
            display_mode_unlock();
                            
            RELEASE_LOCKS = TRUE;
        }        
        
        // ON 'OK' PRESS - SYSTEM MENU - DEFAULTS - LOAD DEFAULT SETTINGS FROM EEPROM
        if( sel_screen == DISPLAY_SYSTEM && sel_line == 3 && ir_command == 0x1C ){        
            
            ir_command = FALSE;
            
            eeprom_w( 0x01, 0 ); // setting to zero will force defaults to be loaded on startup

            flash_backlight( 20, 4 ); // 200ms x 4
                    
            RESET();
        }   

        
        
        
        
    ////////////////////////
    //// THE FLASH MENU ////
    ////////////////////////
        
        // ENTER NEW FLASH INTERVAL VALUE
        if( sel_screen == DISPLAY_FLASH && sel_line == 1 && numb_press ){

            f_int[digit_pos] = (numb_press=='z')?0:numb_press;
            
            digit_pos ++;
            
            load_screen = TRUE;
            
            numb_press  = FALSE;

            // save the altered value
            flash_interval = f_int[0] * 100;
            flash_interval = flash_interval + f_int[1] * 10;
            flash_interval = flash_interval + f_int[2];
            
            if( flash_interval > 150 ){ 
                
                flash_interval = 150;
                f_int[0]=1; f_int[1]=5; f_int[2]=0;
            }
        }
        
        // ENTER NEW FLASH DURATION VALUE
        if( sel_screen == DISPLAY_FLASH && sel_line == 2 && numb_press ){

            f_dur[digit_pos] = (numb_press=='z')?0:numb_press;
            
            digit_pos ++;
            
            load_screen = TRUE;
            
            numb_press  = FALSE;

            // save the altered value
            flash_duration = f_dur[0] * 10;
            flash_duration = flash_duration + f_dur[1];
        }        

        
        
        
    ////////////////////////////
    //// THE UNLOCKING MENU ////
    ////////////////////////////
        
        // ENTER NEW UNLOCKING ATTEMPTS VALUE
        if( sel_screen == DISPLAY_UNLOCKING && sel_line == 1 && numb_press ){

            unlock_attempts = (numb_press=='z')?0:numb_press;
                        
            load_screen = TRUE;
            
            numb_press  = FALSE;
        }
        
        // ENTER NEW UNLOCKING DURATION VALUE
        if( sel_screen == DISPLAY_UNLOCKING && sel_line == 2 && numb_press ){

            u_int[digit_pos] = (numb_press=='z')?0:numb_press;
            
            digit_pos ++;
            
            load_screen = TRUE;
            
            numb_press  = FALSE;

            // save the altered value
            unlock_interval = u_int[0] * 10;
            unlock_interval = unlock_interval + u_int[1];
        }        

        
        
        
              
    ///////////////////////////
    //// THE AUTO OFF MENU ////
    ///////////////////////////
        
        // ENTER NEW AUTO OFF VALUE
        if( sel_screen == DISPLAY_AUTO_OFF && sel_line == 1 && numb_press ){

            s_dur[digit_pos] = (numb_press=='z')?0:numb_press;
                        
            digit_pos ++;
            
            load_screen = TRUE;
            
            numb_press  = FALSE;

            // save the altered value
            shutdown_after = s_dur[0] * 100;
            shutdown_after = shutdown_after + s_dur[1] * 10;
            shutdown_after = shutdown_after + s_dur[2];
            
            // treat shutdown values less than 20 seconds as zero / disable auto off function
            if( shutdown_after < 20 ){
                
                shutdown_after = 0;                
                
                s_dur[0]=0; s_dur[1]=0; s_dur[2]=0;
            }
            
            if( shutdown_after > 255 ){
                
                shutdown_after = 255;
                
                s_dur[0]=2; s_dur[1]=5; s_dur[2]=5;
            }
        } 
        
        
        
        
        
  
        
        
        
        
    ////////////////////////////////
    //// BUTTON PRESS RESPONSES ////
    ////////////////////////////////

        // THE # BUTTON - roll through the display modes
        if( ir_command == 0x0D ){
            
            ir_command = 0;
            
                //sprintf( serial_text, " TRIGGERED! \r\n");
                //tx_string( serial_text );
            

                sel_display ++;
                                
                if( sel_display == 1 ){ BACK_LIGHT = ON; LCD_DISPLAY = ON; }
                
                if( sel_display == 2 ){ 
                    
                    BACK_LIGHT = OFF; 
                    
                    // return to the start if a timer is running,
                    // this creates the effect of just toggling the back light on and off,
                    // which is all we want when navigating the menu system
                    if( TIMER_COUNTDOWN == FALSE ){ sel_display=0; }                
                }
                
                // conceal the display - when timer is running
                if( sel_display == 3 ){ LCD_DISPLAY = OFF; LCD_Clear(); LCD_Display(); }
                
                // reveal the display - when timer is running
                if( sel_display == 4 ){ LCD_DISPLAY = ON; sel_display=0; }
        }
        
        // THE UP KEY - move up menu items
        if( ir_command == 0x18 ){
                        
            ir_command = FALSE;
            
            if( TIMER_COUNTDOWN == FALSE ){
                
                if( sel_line > 0 ){ sel_line--; }else{ sel_line=1; }

                load_screen = TRUE;

                digit_pos = 0;
            }
        }
        
        // THE DOWN KEY -  move down menu items
        if( ir_command == 0x52 ){
            
            ir_command = FALSE;
            
            if( TIMER_COUNTDOWN == FALSE ){

                if( sel_line < 4 ){ sel_line ++; }

                // limit the down presses according to lines available
                if( sel_line == sel_lines+1 ){ sel_line = sel_lines; }

                load_screen = TRUE;

                digit_pos = 0;
            }
        }        
        
        // THE RIGHT KEY - switch between screens going forwards
        if( ir_command == 0x5A ){
            
            ir_command = FALSE;
            
            if( TIMER_COUNTDOWN == FALSE ){

                //BACK_LIGHT = ON;

                if( sel_screen < 7 ){ sel_screen ++; }
                
                sel_line = 1;   // always start at the first line
                
                load_screen = TRUE;
            }            
        }
        
        // THE LEFT KEY - switch between screens going backwards
        if( ir_command == 0x08 ){
            
            ir_command = FALSE;
                        
            if( TIMER_COUNTDOWN == FALSE ){

                //BACK_LIGHT = ON;

                if( sel_screen > 1 ){ sel_screen --; }
                
                sel_line = 1;   // always start at the first line
                
                load_screen = TRUE;
            }            
        }        
        
        // THE * BUTTON - will always cancel any timer and release the locks
        if( ir_command == 0x16 ){
        
            ir_command = FALSE;
            
            TIMER_COUNTDOWN = FALSE;
            
            display_mode_unlock();
            
            RELEASE_LOCKS = TRUE;
        } 
                
        // convert number button presses to their numerical values
        if( ir_command == 0x45 ){ ir_command = 0; numb_press = 1; }
        if( ir_command == 0x46 ){ ir_command = 0; numb_press = 2; }
        if( ir_command == 0x47 ){ ir_command = 0; numb_press = 3; }
        if( ir_command == 0x44 ){ ir_command = 0; numb_press = 4; }
        if( ir_command == 0x40 ){ ir_command = 0; numb_press = 5; }
        if( ir_command == 0x43 ){ ir_command = 0; numb_press = 6; }
        if( ir_command == 0x07 ){ ir_command = 0; numb_press = 7; }
        if( ir_command == 0x15 ){ ir_command = 0; numb_press = 8; }
        if( ir_command == 0x09 ){ ir_command = 0; numb_press = 9; }
        if( ir_command == 0x19 ){ ir_command = 0; numb_press = 'z'; } // 'cos we can't have a real 0  


        
    ////////////////////////////////////////////////
    //// DRIVE THE CLOCK AND UPDATE THE DISPLAY ////
    ////////////////////////////////////////////////
        
        if( clock_ticker && TIMER_COUNTDOWN ){ 

            clock_ticker = 0;

            LCD_Clear();
            
            if( t_minutes < 1 && t_seconds < 0 ){
                t_hours = t_hours - 1;
                t_minutes = 59;
                t_seconds = 59;
            }            

            if( t_seconds < 0 ){
                t_minutes = t_minutes - 1;
                t_seconds = 59;
            }

            if( t_hours < 10 && t_minutes > 9 ){  sprintf( str, "0%i:%i", t_hours, t_minutes ); }
            if( t_hours > 9  && t_minutes < 10 ){ sprintf( str, "%i:0%i", t_hours, t_minutes ); }
            if( t_hours < 10 && t_minutes < 10 ){ sprintf( str, "0%i:0%i", t_hours, t_minutes ); }
            if( t_hours > 9  && t_minutes > 9 ){  sprintf( str, "%i:%i", t_hours, t_minutes ); }            
            
            LCD_TextSize(2);
            LCD_GotoXY(12,15);
            LCD_Print(str);

            if( t_seconds < 10 ){
                sprintf( str, "0%i", t_seconds );
            }else{
                sprintf( str, "%i", t_seconds );
            }

            LCD_TextSize(1);            
            LCD_GotoXY(36,34);
            LCD_Print(str);        

            if( LCD_DISPLAY == ON ){
                
                LCD_Display();
            }

            t_seconds = t_seconds - 1;
            
            
            // flash display according to the specified interval,
            // but only if interval and duration have been set
            
            flash_ticker ++;
             
            if( flash_interval >= 1 && flash_duration >= 1 ){
            
                if( flash_ticker >= flash_interval ){ 

                    BACK_LIGHT = ON; 
                }

                if( flash_ticker >= (flash_interval + flash_duration) ){

                    BACK_LIGHT = OFF;

                    flash_ticker = 0;
                }
            }

            if( flash_ticker > 255 ){ flash_ticker = 0; }
            
            
            // we're done counting down - hand control over to release locks by setting the variable
            
            if( t_hours == 0 && t_minutes == 0 && t_seconds < 0 ){
            
                TIMER_COUNTDOWN = FALSE;
                
                auto_pwr_off = 0;
                
                flash_ticker = 0;
                
                display_mode_unlock();
                
                RELEASE_LOCKS = TRUE;
            }
        }

        
        
        
        
    ////////////////////////////////////
    //// RELEASE LOCKS IF REQUESTED ////
    ////////////////////////////////////
        
        if( RELEASE_LOCKS == TRUE ){
            
            ir_command = FALSE;
                   
            RELEASE_LOCKS = FALSE;
       
            BACK_LIGHT = OFF;       // to conserve power, and improve the odds of a successful unlocking
            
            release_lock( MOTOR_A );
            
            __delay_ms(200);
            
            release_lock( MOTOR_B );
            
            BACK_LIGHT = ON;
            
            __delay_ms(2000);
            
            BACK_LIGHT = OFF;
            
            load_screen = TRUE;     // returns us to whatever screen we started from
                        
            
            // SETUP FOR REPEAT UNLOCKING...
                        
            unlock_count ++;        // count the unlock attempts
            
            unlock_again = 0;       // reset the timer/counter to repeat unlocking
            
            REPEAT_UNLOCK = TRUE;   // check if we need to make further unlock attempts           
        }
        
        
        
    //////////////////////////////////////////////////////////////
    //// RELEASE LOCKS AGAIN IF REPEAT UNLOCKING HAS BEEN SET ////
    //////////////////////////////////////////////////////////////
            
        // if the timer isn't running, and no release locks request is pending and
        // the repeat unlock flag has been set by the unlock routine above
        
        if( TIMER_COUNTDOWN == FALSE && RELEASE_LOCKS == FALSE && REPEAT_UNLOCK == TRUE ){ 

            auto_pwr_off = 0; // reset - we don't want to power off until we've finished unlock attempts
            
            if( unlock_again >= unlock_interval ){
            
                unlock_again = 0;   // reset - the timer will increment this later
                
                RELEASE_LOCKS = TRUE;                
                
                if( unlock_count == unlock_attempts ){
                
                    unlock_count = 0; // reset - until next time
                    
                    REPEAT_UNLOCK = FALSE;
                    
                    RELEASE_LOCKS = FALSE;
                }
            }
        }
      

    } // end while   
    
}

Complete code listing for the Random Time Lock Device, compiled using XC8 V2.45

The above source code for the PIC18F26K22 complete with pic_nokia5110.h header can be downloaded here:-

dual_time_lock_code_aug_22.zip

KiCAD PCB design and Gerber files are available here:-

dual_time_lock_v1.0.zip

dual_time_lock_display_v1.0.zip