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.
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.
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.
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.
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.
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.
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.
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.
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.
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:-