
//#define DISABLE_BUTTONS
//#define DEBUG_MODE
//#define DEBUG_KEYS

#define DIGIT_SET_WRAP_AROUND

#define _SUPPRESS_PLIB_WARNING
#define _DISABLE_OPENADC10_CONFIGPORT_WARNING
//#define _DEBUG
#include <stdio.h>
#include <stdlib.h>
#include <plib.h>
#include <xc.h>
#include "dee_emulation_pic32.h"
#include "parsegps.h"
#include "Timezone/timezones.h"

// DEVCFG3
// USERID = No Setting
#pragma config PMDL1WAY = OFF           // Peripheral Module Disable Configuration (Allow only one reconfiguration)
#pragma config IOL1WAY = ON             // Peripheral Pin Select Configuration (Allow only one reconfiguration)

// DEVCFG1
#pragma config FNOSC = FRC              // Oscillator Selection Bits (Fast RC Osc)
#pragma config FSOSCEN = ON             // Secondary Oscillator Enable (Enabled)
#pragma config IESO = OFF               // Internal/External Switch Over (Diasbled)
#pragma config POSCMOD = OFF            // Primary Oscillator Configuration (External clock/Primary osc disabled)
#pragma config OSCIOFNC = OFF           // CLKO Output Signal Active on the OSCO Pin (Disabled)
#pragma config FPBDIV = DIV_2           // Peripheral Clock Divisor (Pb_Clk is Sys_Clk/2)
#pragma config FCKSM = CSECMD           // Clock Switching and Monitor Selection (Clock Switch Enabled, FSCM Disabled)
#pragma config WDTPS = PS16384          // Watchdog Timer Postscaler (1:16384, ~16s)
#pragma config WINDIS = OFF             // Watchdog Timer Window Enable (Watchdog Timer is in Non-Window Mode)
#pragma config FWDTEN = OFF             // Watchdog Timer Enable (WDT Disabled (SWDTEN Bit Controls))
#pragma config FWDTWINSZ = WINSZ_25     // Watchdog Timer Window Size (Window Size is 25%)

// DEVCFG0
#pragma config DEBUG = OFF              // Background Debugger Enable (Debugger is Enabled)
#pragma config JTAGEN = OFF             // JTAG Enable (JTAG Port Disabled)
#pragma config ICESEL = ICS_PGx3        // ICE/ICD Comm Channel Select (Communicate on PGEC2/PGED2)
#pragma config PWP = OFF                // Program Flash Write Protect (Disable)
#pragma config BWP = OFF                // Boot Flash Write Protect bit (Protection Disabled)
#pragma config CP = OFF                 // Code Protect (Protection Disabled)


#define SYS_FREQ (8000000L)
#define BUTTON_DEBOUNCE_TIME 20
#define LONG_BUTTON_TIME     256

/*
 Usage:
 o To set time/date:
   * Long press L button
   * Current digit flashes. Short press L button to increment, short press R button to change digits.
   * Long press R button to switch between time and date
   * Long press both buttons together to switch between 12 & 24 hour time
   * When finished, long press L button - timer resumes upon release
 o To set the time zone:
   * Time zone only required for GPS operation
   * Time zone is determined automatically based on lat/long for Australia, NZ & UK
   * Daylight savings also applied automatically using rules as of December 2014
   * To override time zone, once GPS time has been picked up, long press L button.
   * Use L/R to change time zone offset in 15 minute increments. "  00  " means automatic.
   * Long press L button to save. Time will be updated next time GPS data is available.
   * Long press R button to abort changing time zone.
 o To trim crystal:
   * Long press both buttons together.
   * Trim value is displayed. 500 means no adjustment. Higher values make the clock run faster, lower values slower. Max 999, min 000.
   * Adjust trim value using l & r buttons to decrement/increment.
   * Long press L button to set new trim value, long press R button to abort.
 o To display date:
   * Short press L button. Date will be displayed for 10 seconds.
 o To set alarm time:
   * Long press R button.
   * Set as per setting time.
   * To change day of week on which alarm sounds, long press R button.
     * Days are represented as 0-6 (Sun-Sat). Default is Mon-Fri. Press L to toggle a day on or off and R to proceed to next day. Display scrolls to show all seven.
   * When finihed, long press R button.
 o To display alarm time or turn alarm on/off:
   * Short press R button, alarm time will be displayed for 10 seconds. It will flash if not set to go off.
   * Short press L button while time displayed to switch alarm on or off.
 o When alarm goes off:
   * Buzzer beeps at 1Hz, 50% duty cycle for one minute. Display and LEDs go to full brightness and flash at 1Hz.
   * Wave hand in front of proximity sensor for 5 minute snooze
   * Press either button to stop alarm
 o To change leading zero blanking:
   * Long press both buttons together (goes into crystal trimming mode), then repeat to exit this mode and change leading zero blanking setting.
 o To turn blue LEDs on/off:
   * Short press both buttons together.
 o To enable/disable auto-dimming of digits:
   * Hold down L button for at least one second, then press the R button and quickly release both.
   * ...
 o To enable/disable auto-dimming of LEDs:
   * Hold down R button for at least one second, then press the L button and quickly release both.
   * ...

 o Settings that are stored in EEPROM:
   * Crystal trim
   * 12/24 hour mode
   * Leading zero blanking
   * LED state
   * Dimming values
   * Alarm settings
   * Lime zone override
 */

char display[6+1] = "      ";
#define MAX_BRIGHT 8
unsigned char brightness[6] = { MAX_BRIGHT, MAX_BRIGHT, MAX_BRIGHT, MAX_BRIGHT, MAX_BRIGHT, MAX_BRIGHT };
char led_bright = MAX_BRIGHT, buzzer_on, leds_on, time_set;

static const unsigned char days_in_month[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
static unsigned char get_days_in_month(unsigned char month, unsigned int year) {
    if( month == 2 && !(year&3) && ((year%100) || !(year%400)) )
        return 29;
    else
        return days_in_month[month-1];
}
static unsigned char dec_to_bcd(unsigned char dec) {
    return ((dec/10)<<4)|(dec%10);
}
static unsigned char bcd_to_dec(unsigned char bcd) {
    return (bcd>>4)*10 + (bcd&0x0F);
}

unsigned short S1down, S2down, IRTestStartTMR1, IRTestFinishTMR1, IRTestDuration, IRTestSum, IRTestCount;
unsigned char last_button_state, buttons_down, lbutton_short_press, lbutton_long_press, rbutton_short_press, rbutton_long_press, bbutton_short_press, bbutton_long_press, bbutton_shortlong_press_lr, bbutton_shortlong_press_rl, proximity_sense, proximity_timeout;
volatile unsigned char IRTesting;
int gps_forward_disable;

// default ModulationFrequency = 38000, DutyCycle = 14
static void InitIRLED(unsigned int ModulationFrequency, unsigned int DutyCycle) {
    unsigned int Divisor;

    if( ModulationFrequency < 30000 )
        ModulationFrequency = 30000;
    else if( ModulationFrequency > 50000 )
        ModulationFrequency = 50000;
    if( DutyCycle < 1 )
        DutyCycle = 1;
    else if( DutyCycle > 99 )
        DutyCycle = 99;

    TRISBbits.TRISB0 = 0;
    LATBbits.LATB0 = 1;
    ANSELBbits.ANSB0 = 0;
    RPB0R = 5; // RB0 = OC3
    T2CONbits.TON = 1;
    Divisor = 8000000 / ModulationFrequency;//210;
    PR2 = Divisor - 1;
    OC3RS = Divisor * (100 - DutyCycle) / 100;//180;
    OC3CONbits.OCM = 6; // PWM
}
static void StartIRTest() {
    while( SPI1STATbits.SPIBUSY )
        ;
    SPI1CONbits.MSTEN = 0;
    TRISBbits.TRISB14 = 1;
    OC3CONbits.ON = 1;
    IRTestStartTMR1 = TMR1;
    if( !PORTBbits.RB14 )
        IRTesting = 2;
    else
        IRTesting = 1;
}
static void FinishIRTest() {
    IRTestFinishTMR1 = TMR1;
    IRTesting = 0;
    OC3CONbits.ON = 0;
    SPI1CONbits.MSTEN = 1;
    TRISBbits.TRISB14 = 0;
}
static void PauseIRTest() {
    SPI1CONbits.MSTEN = 1;
    TRISBbits.TRISB14 = 0;
}
static void ResumeIRTest() {
    while( SPI1STATbits.SPIBUSY )
        ;
    SPI1CONbits.MSTEN = 0;
    TRISBbits.TRISB14 = 1;
}

void __ISR( _CHANGE_NOTICE_VECTOR, ipl2auto) DetButtons(void) {
    if( IFS1bits.CNAIF ) {
        if( PORTAbits.RA2 ) {
            // 1pps pulse
            rtccTime time;
            rtccDate date;
            int halfsec;

            while(1) {
                time.l = RTCTIME;
                date.l = RTCDATE;
                halfsec = RTCCONbits.HALFSEC;
                if( time.l == RTCTIME && date.l == RTCDATE && halfsec == RTCCONbits.HALFSEC )
                    break;
            }
            if( gps_forward_disable )
                --gps_forward_disable;
            if( halfsec && !gps_forward_disable ) {
                if( ((++time.sec)&15) == 10 )
                    time.sec += 6;
                if( time.sec == 0x60 ) {
                    time.sec = 0;
                    if( ((++time.min)&15) == 10 )
                        time.min += 6;
                    if( time.min == 0x60 ) {
                        time.min = 0;
                        if( ((++time.hour)&15) == 10 )
                            time.hour += 6;
                        if( time.hour == 0x24 ) {
                            time.hour = 0;
                            if( ++date.wday == 7 )
                                date.wday = 0;
                            if( ((++date.mday)&15) == 10 )
                                date.mday += 6;

                            if( bcd_to_dec(date.mday) > get_days_in_month(bcd_to_dec(date.mon), 2000+bcd_to_dec(date.year)) ) {
                                date.mday = 1;
                                if( (++date.year&15) == 10 )
                                    date.year += 6;
                            }
                        }
                    }
                }
                if( RTCCONbits.HALFSEC && !RTCCONbits.RTCSYNC ) {
                    RTCTIME = time.l;
                    RTCDATE = date.l;
                    gps_forward_disable = 1000;
                }
            } else {
                RTCTIME = time.l;
            }
        }
        IFS1bits.CNAIF = 0;
    }
    if( IFS1bits.CNBIF ) {
#ifndef DISABLE_BUTTONS
        if( !PORTBbits.RB5 && !(last_button_state&1) ) {
            if( !(buttons_down&1) )
                S1down = TMR1;
            last_button_state |= 1;
            buttons_down |= 1;
        } else if( PORTBbits.RB5 && (last_button_state&1) ) {
            last_button_state &= ~1;
            if( last_button_state == 0 ) {
                if( TMR1 - S1down > BUTTON_DEBOUNCE_TIME ) {
                    unsigned short time_down = TMR1;
                    time_down -= S1down;
                    if( buttons_down&2 ) {
                        unsigned short other_time_down = TMR1;
                        other_time_down -= S2down;
                        if( time_down > LONG_BUTTON_TIME && other_time_down > LONG_BUTTON_TIME )
                            ++bbutton_long_press;
                        else if( time_down > LONG_BUTTON_TIME )
                            ++bbutton_shortlong_press_rl;
                        else
                            ++bbutton_short_press;
                    } else {
                        if( time_down > LONG_BUTTON_TIME )
                            ++lbutton_long_press;
                        else
                            ++lbutton_short_press;
                    }
                }
                buttons_down = 0;
            }
        }
        if( !PORTBbits.RB6 && !(last_button_state&2) ) {
            if( !(buttons_down&2) )
                S2down = TMR1;
            last_button_state |= 2;
            buttons_down |= 2;
        } else if( PORTBbits.RB6 && (last_button_state&2) ) {
            last_button_state &= ~2;
            if( last_button_state == 0 ) {
                if( TMR1 - S2down > BUTTON_DEBOUNCE_TIME ) {
                    unsigned short time_down = TMR1;
                    time_down -= S2down;
                    if( buttons_down&1 ) {
                        unsigned short other_time_down = TMR1;
                        other_time_down -= S1down;
                        if( time_down > LONG_BUTTON_TIME && other_time_down > LONG_BUTTON_TIME )
                            ++bbutton_long_press;
                        else if( time_down > LONG_BUTTON_TIME )
                            ++bbutton_shortlong_press_lr;
                        else
                            ++bbutton_short_press;
                    } else {
                        if( time_down > LONG_BUTTON_TIME )
                            ++rbutton_long_press;
                        else
                            ++rbutton_short_press;
                    }
                }
                buttons_down = 0;
            }
        }
#endif
        IFS1bits.CNBIF = 0;
    }
}
static void InitButtons() {
    //  left button = S1 = RB5
    // right button = S2 = RB6
    TRISBbits.TRISB5 = 1;
    TRISBbits.TRISB6 = 1;
    CNPUBbits.CNPUB5 = 1;
    CNPUBbits.CNPUB6 = 1;
    T1CONbits.TCS = 1;
    T1CONbits.TSYNC = 1;
    T1CONbits.TCKPS = 2; // divide by 64
    T1CONbits.TON = 1;
    CNENBbits.CNIEB5 = 1;
    CNENBbits.CNIEB6 = 1;
    CNCONBbits.ON = 1;
    IFS1bits.CNBIF = 0;
    IEC1bits.CNBIE = 1;
    IPC8bits.CNIP = 2;
    PORTB;
}

static void Init1PPS() {
    TRISAbits.TRISA2 = 1;
    CNPDAbits.CNPDA2 = 1;
    CNENAbits.CNIEA2 = 1;
    CNCONAbits.ON = 1;
    IFS1bits.CNAIF = 0;
    IEC1bits.CNAIE = 1;
    IPC8bits.CNIP = 2;
    PORTA;
}

static void InitOutputs() {
    LATBbits.LATB12 = 0;
    TRISBbits.TRISB12 = 0;
    ANSELBbits.ANSB12 = 0;
    LATBbits.LATB11 = 0;
    TRISBbits.TRISB11 = 0;
    LATBbits.LATB10 = 0;
    TRISBbits.TRISB10 = 0;
    LATBbits.LATB7 = 0;
    TRISBbits.TRISB7 = 0;
    LATBbits.LATB8 = 0;
    TRISBbits.TRISB8 = 0;
    LATBbits.LATB9 = 0;
    TRISBbits.TRISB9 = 0;
    LATBbits.LATB1 = 0;
    TRISBbits.TRISB1 = 0;
    ANSELBbits.ANSB1 = 0;
    LATBbits.LATB2 = 0;
    TRISBbits.TRISB2 = 0;
    ANSELBbits.ANSB2 = 0;
    LATBbits.LATB3 = 0;
    TRISBbits.TRISB3 = 0;
    ANSELBbits.ANSB3 = 0;
    LATAbits.LATA0 = 1;
    TRISAbits.TRISA0 = 0;
    ANSELAbits.ANSA0 = 0;
    LATAbits.LATA1 = 1;
    TRISAbits.TRISA1 = 0;
    ANSELAbits.ANSA1 = 0;
    LATAbits.LATA3 = 1;
    TRISAbits.TRISA3 = 0;

    RPA1R = 3; // SDO1 = RA1

    LATBbits.LATB14 = 0;
    TRISBbits.TRISB14 = 0;
    ANSELBbits.ANSB14 = 0;
    SPI1CONbits.DISSDI = 1;
    SPI1CONbits.MSSEN = 1;
    SPI1CONbits.MSTEN = 1;
    SPI1CONbits.CKP = 0;
    SPI1CONbits.SSEN = 0;
    SPI1CONbits.CKE = 1;
    SPI1CONbits.MODE16 = 0;
    SPI1CONbits.MODE32 = 0;
    SPI1CONbits.ENHBUF = 1;
    SPI1CONbits.ON = 1;

    LATAbits.LATA0 = 0;
    SPI1BUF = 0;
    SPI1BUF = 0;
    SPI1BUF = 0;
    SPI1BUF = 0;
    SPI1BUF = 0;
    while( SPI1STATbits.SPIBUSY )
        ;
    LATAbits.LATA0 = 1;
    LATAbits.LATA3 = 0;
}
static void UpdateOutputs(unsigned char brightness_counter) {
    unsigned char data[5];
    unsigned long temp_latb;
    // digits are wired like this:
    //
    // 10 1263478950 015234 0152367849 015234 0152367849

    LATAbits.LATA0 = 0;
    temp_latb = LATB & ~( (1<<12) | (1<<11) | (1<<10) | (1<<7) | (1<<8) | (1<<9) | (1<<1) | (1<<2) | (1<<3) );
    data[0] = 0;
    data[1] = 0;
    data[2] = 0;
    data[3] = 0;
    data[4] = 0;
    switch(brightness_counter < brightness[5] ? display[5] : ' ') {
        case '9':
            temp_latb |= (1<<12);
            break;
        case '4':
            temp_latb |= (1<<11);
            break;
        case '8':
            temp_latb |= (1<<10);
            break;
        case '7':
            temp_latb |= (1<<7);
            break;
        case '6':
            temp_latb |= (1<<8);
            break;
        case '3':
            temp_latb |= (1<<9);
            break;
        case '2':
            temp_latb |= (1<<1);
            break;
        case '5':
            temp_latb |= (1<<2);
            break;
        case '1':
            temp_latb |= (1<<3);
            break;
        case '0':
            data[0] |= (1<<6);
            break;
    }
    switch(brightness_counter < brightness[4] ? display[4] : ' ') {
        case '4':
            data[0] |= (1<<5);
            break;
        case '3':
            data[0] |= (1<<4);
            break;
        case '2':
            data[0] |= (1<<3);
            break;
        case '5':
            data[0] |= (1<<2);
            break;
        case '1':
            data[0] |= (1<<1);
            break;
        case '0':
            data[1] |= (1<<7);
            break;
    }
    if( leds_on && brightness_counter < led_bright )
        data[0] |= (1<<0);
    SPI1BUF = data[0];

    switch(brightness_counter < brightness[3] ? display[3] : ' ') {
        case '9':
            data[1] |= (1<<6);
            break;
        case '4':
            data[1] |= (1<<5);
            break;
        case '8':
            data[1] |= (1<<4);
            break;
        case '7':
            data[1] |= (1<<3);
            break;
        case '6':
            data[1] |= (1<<2);
            break;
        case '3':
            data[1] |= (1<<1);
            break;
        case '2':
            data[2] |= (1<<7);
            break;
        case '5':
            data[2] |= (1<<6);
            break;
        case '1':
            data[2] |= (1<<5);
            break;
        case '0':
            data[2] |= (1<<4);
            break;
    }
    if( buzzer_on )
        data[1] |= (1<<0);
    SPI1BUF = data[1];

    switch(brightness_counter < brightness[2] ? display[2] : ' ') {
        case '4':
            data[2] |= (1<<3);
            break;
        case '3':
            data[2] |= (1<<2);
            break;
        case '2':
            data[2] |= (1<<1);
            break;
        case '5':
            data[3] |= (1<<7);
            break;
        case '1':
            data[3] |= (1<<6);
            break;
        case '0':
            data[3] |= (1<<5);
            break;
    }
    switch(brightness_counter < brightness[0] ? display[0] : ' ') {
        case '3':
            data[2] |= (1<<0);
            break;
    }
    SPI1BUF = data[2];

    switch(brightness_counter < brightness[1] ? display[1] : ' ') {
        case '0':
            data[3] |= (1<<4);
            break;
        case '5':
            data[3] |= (1<<3);
            break;
        case '9':
            data[3] |= (1<<2);
            break;
        case '8':
            data[3] |= (1<<1);
            break;
        case '7':
            data[4] |= (1<<7);
            break;
        case '4':
            data[4] |= (1<<6);
            break;
        case '3':
            data[4] |= (1<<5);
            break;
        case '6':
            data[4] |= (1<<4);
            break;
        case '2':
            data[4] |= (1<<3);
            break;
        case '1':
            data[4] |= (1<<2);
            break;
    }
    switch(brightness_counter < brightness[0] ? display[0] : ' ') {
        case '2':
            data[3] |= (1<<0);
            break;
    }
    SPI1BUF = data[3];

    switch(brightness_counter < brightness[0] ? display[0] : ' ') {
        case '0':
            data[4] |= (1<<1);
            break;
        case '1':
            data[4] |= (1<<0);
            break;
    }
    SPI1BUF = data[4];

    while( SPI1STATbits.SPIBUSY )
        ;
    LATAbits.LATA0 = 1;
    LATB = temp_latb;
}

static void SendSerialString(const char* string) {
    while( *string ) {
        U1TXREG = *string;
        while( U1STAbits.UTXBF )
            ;
        ++string;
    }
}

#ifdef DEBUG_MODE
void Debug_SendSerialString(const char* string) {
    while( *string ) {
        U1TXREG = *string;
        while( U1STAbits.UTXBF )
            ;
        ++string;
    }
    U1TXREG = '\n';
    while( U1STAbits.UTXBF )
        ;
}
#endif

static int check_supply_voltage() {
    int val;
//  char string[16];
    TRISAbits.TRISA1 = 1;
    ANSELAbits.ANSA1 = 1;
    SPI1CONbits.DISSDO = 1;
    RPA1R = 0; // SDO1 != RA1

    IFS0bits.AD1IF = 0;
    AD1CHSbits.CH0SA = 1;
    AD1CON3bits.SAMC = 1;//OSCCONbits.COSC == 5 ? 1 : 8; // don't need as long a sampling
    AD1CON1bits.SSRC = 7;//                            // period when running from LPRC
    AD1CON1bits.ON = 1;

//    val = 0;
    do {
        AD1CON1bits.SAMP = 1;
        while( !IFS0bits.AD1IF )
            ;
        val = ADC1BUF0;
    } while( val > 400 );

//  sprintf(string, "b%d", val);
//  Debug_SendSerialString(string);

    SPI1CONbits.DISSDO = 0;
    if( OSCCONbits.COSC != 5 ) {
        // don't restore pin to SPI function if processor just came out of sleep
        RPA1R = 3; // SDO1 = RA1
        ANSELAbits.ANSA1 = 0;
        TRISAbits.TRISA1 = 0;
    }
#ifdef _DEBUG
    val = 300;
#endif
#ifdef DEBUG_MODE
    val = 300;
#endif
    return val;
}

static int measure_LDR() {
    int val;
#ifdef DEBUG_MODE
    char string[16];
#endif
    TRISAbits.TRISA0 = 1;
    ANSELAbits.ANSA0 = 1;

    IFS0bits.AD1IF = 0;
    AD1CON1bits.ON = 0;
    AD1CHSbits.CH0SA = 0;
    AD1CON3bits.SAMC = 2;
    AD1CON1bits.SSRC = 7;
    AD1CON1bits.ON = 1;

    AD1CON1bits.SAMP = 1;
    while( !IFS0bits.AD1IF )
        ;
    val = ADC1BUF0;

#ifdef DEBUG_MODE
    sprintf(string, "l%d", val);
    Debug_SendSerialString(string);
#endif

    if( OSCCONbits.COSC != 5 ) {
        // don't restore pin to SPI function if processor just came out of sleep
        ANSELAbits.ANSA0 = 0;
        TRISAbits.TRISA0 = 0;
    }
    return val;
}

static void setup_serial(unsigned char invert, unsigned int baud_rate) {
    LATBbits.LATB13 = 0;
    ANSELBbits.ANSB13 = 0;
    LATBbits.LATB15 = 0;
    TRISBbits.TRISB15 = 0;
    ANSELBbits.ANSB15 = 0;
    // map RB13 (3) to U1RX
    U1RXR = 3;
    // map RB15 to U1TX (1)
    RPB15R = 1;

    U1MODE = 0;
    U1BRG = 500000 / baud_rate - 1; // Fpb = 4MHz
    U1STAbits.UTXEN = 1;
    U1STAbits.URXEN = 1;
    U1MODEbits.RXINV = invert;
    U1MODEbits.ON = 1;
    U1TXREG = '\n';
}

static void close_serial() {
    while( !U1STAbits.TRMT )
        ;
    ANSELBbits.ANSB13 = 1;
    TRISBbits.TRISB15 = 1;
    ANSELBbits.ANSB15 = 1;
    U1STAbits.UTXEN = 0;
    U1STAbits.URXEN = 0;
    U1MODEbits.ON = 0;

    // unmap RB13 (3) from U1RX
    U1RXR = 0;
    // unmap RB15 from U1TX (1)
    RPB15R = 0;
}

static const unsigned char dow_table[12] = { 0, 3, 3, 6, 1, 4, 6, 2, 5, 0, 3, 5 };
static unsigned char get_dow(unsigned char day, unsigned char month, unsigned char year) {
    return (day + dow_table[month-1] + year + (year>>2) + 6 - (month <= 2 && !(year&3) ? 1 : 0)) % 7;
}

static void LowPowerSleep() {
    unsigned int status1, status2;
    unsigned long ad1con1, oc3con, cncona, cnconb, spi1con, t1con, t2con;
    unsigned long tra, ana, trb, anb, pua, pda, pub, pdb;

    ClearWDT();
    // save state
    ad1con1 = AD1CON1;
    oc3con = OC3CON;
    cncona = CNCONA;
    cnconb = CNCONB;
    spi1con = SPI1CON;
    t1con = T1CON;
    t2con = T2CON;
    tra = TRISA;
    ana = ANSELA;
    trb = TRISB;
    anb = ANSELB;
    pua = CNPUA;
    pda = CNPDA;
    pub = CNPUB;
    pdb = CNPDB;

    // turn stuff off
    AD1CON1bits.ON = 0;
    OC3CONbits.ON = 0;
    CNCONAbits.ON = 0;
    CNCONBbits.ON = 0;
    SPI1CONbits.ON = 0;
    T1CONbits.TON = 0;
    T2CONbits.TON = 0;
/*
    // enable faster wake-up due to power being restored as detected by digital input
    LATAbits.LATA1 = 0;
    TRISAbits.TRISA1 = 1;
    ANSELAbits.ANSA1 = 0;
    CNENA = 0;
    CNENB = 0;
    if( !PORTAbits.RA1 ) {
        CNENAbits.CNIEA1 = 1;
        CNCONAbits.ON = 1;
        IFS1bits.CNAIF = 0;
        IEC1bits.CNAIE = 1;
        IPC8bits.CNIP = 2;
    }
*/
    mSYSTEMUnlock(status1, status2);
    if( OSCCONbits.COSC != 5 ) {
      OSCCONbits.NOSC = 5; // switch to LPRC
      OSCCONbits.OSWEN = 1;
    }
    // prepare to sleep
    OSCCONbits.SLPEN = 1;
    // set power down bits for most peripherals
    CFGCONbits.PMDLOCK = 0;
    PMD1 = 0xFFFFFFFF;
    PMD2 = 0xFFFFFFFF;
    PMD3 = 0xFFFFFFFF;
    PMD4 = 0xFFFFFFFF;
    PMD5 = 0xFFFFFFFF;
    PMD6 = 0xFFFFFFFE; // leave RTCC on
    // disable outputs
    TRISA = 0xFFFFFFFF;
    ANSELA = 0xFFFFFFFD;
    TRISB = 0xFFFFFFFF;
    ANSELB = 0xFFFFFFFF;
    CNPUA = 0x00000008;
    CNPDA = 0xFFFFFFE0;
    CNPUB = 0;
    CNPDB = 0xFFFFFFEF;
    mSYSTEMLock(status1, status2);
    // wait for clock switch to complete (if in progress)
    while( OSCCONbits.OSWEN )
        ;
    // sleep
    EnableWDT();
    asm volatile("wait");
    DisableWDT();

    // turn power-off bits off
    mSYSTEMUnlock(status1, status2);
    PMD1 = 0;
    PMD2 = 0;
    PMD3 = 0;
    PMD4 = 0;
    PMD5 = 0;
    PMD6 = 0;
    CFGCONbits.PMDLOCK = 1;
    OSCCONbits.SLPEN = 0;
    mSYSTEMLock(status1, status2);

    // restore registers
    AD1CON1 = ad1con1;
    OC3CON = oc3con;
    CNCONA = cncona;
    CNCONB = cnconb;
    SPI1CON = spi1con;
    T1CON = t1con;
    T2CON = t2con;
    TRISA = tra;
    ANSELA = ana;
    TRISB = trb;
    ANSELB = anb;
    CNPUA = pua;
    CNPDA = pda;
    CNPUB = pub;
    CNPDB = pdb;
}

rtccTime time, last_time, alarm_time, cur_alarm_time;
rtccDate date;
char halfSec, last_halfSec, alarm_day_mask;
char time_set_digit, set_date, show_date, set_alarm, show_alarm, alarm_on, set_alarm_day, digit_test_mode, set_tz, set_nixie_brightness, set_led_brightness, immediate_update;
int crystal_trim = 500, old_crystal_trim;
char set_trim, set_proximity_sensitivity, proximity_trigger;
char alarm_timer;
char _24_hour_display = 1, _24_hour_show_timer, leading_zero_blanking, leds_enabled = 1;
int itemp, ldr_val = 8, num_ldr_vals, cur_ldr_val, nixie_brightness = 8, led_brightness = 8, new_brightness;
unsigned short ldr_vals[32];
unsigned int temp;
unsigned long serial_rx_bytes_since_last_valid;
unsigned int ProximityTriggerThreshold = 40, ProximityModulationFrequency = 38000, ProximityDutyCycle = 14;

int gps_fix, gps_time, gps_fractime, gps_date, gps_lat, gps_lon, gps_detected, gps_fixgood, gps_datetimelatlongood, gps_fixgoodfor, gps_good_counter;
char gps_alt[16];
char gpsbuf[256];
int gpsbuf_pos;
int tz_searching, timezone_found = -1, tz_override = -1, new_tz_override = -1, cycles_without_serial_rx;

int main(int argc, char** argv) {
    if( (RCON & 0x18) == 0x18 ) {
        // The WDT caused a wake from sleep
        RCONCLR = 0x18;
        asm volatile ( "eret" ); // return from interrupt
    }
    if( (RCON & 0x14) == 0x14 ) {
        // The WDT caused a wake from idle
        RCONCLR = 0x14;
        asm volatile ( "eret" ); // return from interrupt
    }

    time.l = 0;
    date.l = 0;

    SYSTEMConfigPerformance(SYS_FREQ);
    INTEnableSystemMultiVectoredInt();
    INTEnableInterrupts();

    mOSCEnableSOSC();

    DataEEInit();
    alarm_time.l = 0;
    cur_alarm_time.l = 0;
    alarm_day_mask = 0x3E;
    if( DataEERead(&temp, 1) == 0 ) {
        crystal_trim = temp&1023;
        _24_hour_display = (temp>>10)&1;
        leading_zero_blanking = (temp>>11)&1;
        leds_enabled = (temp>>12)&1;
        alarm_on = (temp>>13)&1;
        alarm_day_mask = (temp>>14)&0x7F;
    }
    if( DataEERead(&temp, 2) == 0 ) {
        alarm_time.l = temp;
        cur_alarm_time.l = temp;
    }
    if( DataEERead(&itemp, 3) == 0 && itemp >= -1 && itemp <= 2345 ) {
        tz_override = itemp;
    }
    if( DataEERead(&temp, 4) == 0 && temp < 20 )
        nixie_brightness = temp;
    if( DataEERead(&temp, 5) == 0 && temp < 20 )
        led_brightness = temp;
    if( DataEERead(&temp, 6) == 0 ) {
        ProximityModulationFrequency = 30000 + (temp&255) * 100;
        ProximityDutyCycle = (temp>>8)&255;
        ProximityTriggerThreshold = (temp>>16)&255;
        if( ProximityTriggerThreshold < 10 )
            ProximityTriggerThreshold = 10;
        else if( ProximityTriggerThreshold > 200 )
            ProximityTriggerThreshold = 200;
    }

    while( !OSCCONbits.SOSCRDY )
        ;
    RtccInit();
    while( RtccGetClkStat() != RTCC_CLK_ON )
        ;
    RtccSetCalibration(crystal_trim-500);

    setup_serial(0, 9600);

    digit_test_mode = 1;
    date.mday = 0x01;
    date.mon = 0x01;
    date.year = 0x15;
    while(1) {
        // initialise
        InitIRLED(ProximityModulationFrequency, ProximityDutyCycle);
        InitButtons();
        InitOutputs();
        Init1PPS();
        last_time.l = 0;
        last_halfSec = 0;

        // main loop
        while( 1 ) {
            int val;
    //      char string[16];
            if( time_set ) {
                time.l = RtccGetTime();
                date.l = RtccGetDate();
            }
            halfSec = mRtccGetHalfSecond();
            if( time.l == last_time.l && ((time_set && !set_alarm && (!show_alarm || alarm_on) && !set_trim && !alarm_timer && !_24_hour_show_timer && !digit_test_mode && !set_tz && !set_nixie_brightness && !set_led_brightness && !set_proximity_sensitivity
                                                                                               && !(gps_detected && (!gps_fixgood || !gps_datetimelatlongood))) || halfSec == last_halfSec) && !immediate_update ) {

                if( IRTesting ) {
                    if( !PORTBbits.RB14 ) {
                        if( IRTesting == 1 )
                            IRTesting = 2;
                    } else if( IRTesting == 2 ) {
                        IRTestDuration = TMR1 - IRTestStartTMR1;
                        if( IRTestDuration > 1 ) {
                            FinishIRTest();
                            IRTesting = 3;
                        }
                    }
                }

                if( brightness[0] != MAX_BRIGHT || brightness[1] != MAX_BRIGHT || brightness[2] != MAX_BRIGHT ||
                    brightness[3] != MAX_BRIGHT || brightness[4] != MAX_BRIGHT || brightness[5] != MAX_BRIGHT ) {
                    if( IRTesting == 1 || IRTesting == 2 )
                        PauseIRTest();
                    UpdateOutputs(TMR1%MAX_BRIGHT);
                    if( IRTesting == 1 || IRTesting == 2 )
                        ResumeIRTest();
                }

                if( U1STAbits.OERR )
                    U1STAbits.OERR = 0;
                if( !IRTesting ) {
                    unsigned short duration = TMR1 - IRTestFinishTMR1;
                    if( duration >= 32 )
                        StartIRTest();
                }


                if( U1STAbits.URXDA ) {
                    cycles_without_serial_rx = 0;
                    while( U1STAbits.URXDA ) {
                        ++serial_rx_bytes_since_last_valid;
                        if( serial_rx_bytes_since_last_valid > 4096 ) {
                            // If we get more than 1000 characters of serial data but no valid GPS messages,
                            // try re-initialising the serial port at a different baud rate (4800/9600) or
                            // inverted. One of the four combos should work.
                            unsigned char inverted = U1MODEbits.RXINV;
                            unsigned int baud_rate = 500000 / ((unsigned int)U1BRG + 1);
                            close_serial();
                            setup_serial(!inverted, inverted ? (baud_rate > 5000 ? 9600 : 4800) : (baud_rate > 5000 ? 4800 : 9600));
                            gpsbuf_pos = 0;
                            serial_rx_bytes_since_last_valid = 0;
                        }
                        if( gpsbuf_pos ) {
                            if( gpsbuf_pos == sizeof(gpsbuf)-2 ) {
                                gpsbuf_pos = 0;
                            } else {
                                gpsbuf[gpsbuf_pos] = U1RXREG;
                                ++gpsbuf_pos;
                                if( gpsbuf_pos > 3 && gpsbuf[gpsbuf_pos-2] == '\r' && gpsbuf[gpsbuf_pos-1] == '\n' ) {
                                    gpsbuf[gpsbuf_pos] = '\0';
                                    gpsbuf_pos = 0;
                                    switch( ParseGPS(gpsbuf, &gps_fix, gps_alt, &gps_time, &gps_fractime, &gps_date, &gps_lat, &gps_lon) ) {
                                        case GPS_MSG_FIXANDALT:
                                            gps_detected = 1;
                                            if( gps_fix > 0 )
                                                gps_fixgood = 10;
                                            serial_rx_bytes_since_last_valid = 0;
                                            break;
                                        case GPS_MSG_TIMEDATELATLON:
                                            gps_detected = 1;
                                            gps_datetimelatlongood = 10;
                                            if( timezone_found != -1 ) {
                                                int gtime = gps_time, gdate = gps_date;
                                                rtccTime rtime;
                                                rtccDate rdate;
                                                if( timezone_found != -2 && gps_fixgoodfor > 3 ) {
                                                    if( tz_override != -1 ) {
                                                        add_timezone_offset(&gtime, &gdate, tz_override);
                                                    } else {
                                                        apply_timezone(&gtime, &gdate, timezone_found);
                                                    }
                                                }

                                                rdate.year = dec_to_bcd(gdate%100);
                                                rdate.mon = dec_to_bcd((gdate/100)%100);
                                                rdate.mday = dec_to_bcd((gdate/10000));
                                                rtime.sec = dec_to_bcd(gtime%100);
                                                rtime.min = dec_to_bcd((gtime/100)%100);
                                                rtime.hour = dec_to_bcd((gtime/10000));
                                                if( rtime.l != time.l || rdate.l != date.l ) {
                                                    time.l = rtime.l;
                                                    date.l = rdate.l;
                                                    RtccSetTimeDate(time.l, date.l);
                                                    time_set = 1;
                                                }
                                            }
                                            serial_rx_bytes_since_last_valid = 0;
                                            break;
                                    }
                                }
                            }
                        } else {
                            char letter = U1RXREG;
                            switch(letter) {
#ifdef DEBUG_KEYS
                                case 'l':
                                    ++lbutton_short_press;
                                    break;
                                case 'L':
                                    ++lbutton_long_press;
                                    break;
                                case 'r':
                                    ++rbutton_short_press;
                                    break;
                                case 'R':
                                    ++rbutton_long_press;
                                    break;
                                case 'b':
                                    ++bbutton_short_press;
                                    break;
                                case 'B':
                                    ++bbutton_long_press;
                                    break;
                                case 'p':
                                    ++proximity_sense;
                                    break;
                                case 'x':
                                    ++bbutton_shortlong_press_rl;
                                    break;
                                case 'y':
                                    ++bbutton_shortlong_press_lr;
                                    break;
#endif//DEBUG_KEYS
                                case '$':
                                    gpsbuf[0] = '$';
                                    gpsbuf_pos = 1;
                                    break;
                            }
                        }
                    }
                } else if( gps_fixgood && gps_datetimelatlongood ) {
                    int ret;
                    if( !tz_searching ) {
                        tz_searching = 1;
                        init_get_timezone(gps_lat, gps_lon);
                    }
                    ret = continue_get_timezone(8);
                    if( ret != -1 ) {
                        timezone_found = ret;
                        tz_searching = 0;
                    }
                    if( gps_fixgoodfor < 10 )
                        ++gps_fixgoodfor;
                    gps_good_counter = 8*3600;
                } else {
                    gps_fixgoodfor = 0;
                    if( ++cycles_without_serial_rx > 4000000 ) {
                        cycles_without_serial_rx -= 400000;
                        SendSerialString("\r\n");
//                        U1TXREG = '\r'
//                        U1TXREG = '\n';
                    }
                }

                if( IRTesting == 3 ) {
                    IRTestSum += IRTestDuration;
                    IRTestCount += 1;
                    IRTesting = 0;
                }
                // break out early in response to proximity sensor triggering to show date
                if( (IRTestCount < 2 || IRTestSum / IRTestCount < ProximityTriggerThreshold/*40*/) &&
                    !lbutton_short_press && !lbutton_long_press && !rbutton_short_press && !rbutton_long_press && !bbutton_short_press && !bbutton_shortlong_press_lr && !bbutton_shortlong_press_rl && !proximity_sense )
                    continue;
            }
            if( IRTesting )
                FinishIRTest();
            if( IRTestCount ) {
                if( IRTestSum / IRTestCount >= ProximityTriggerThreshold/*40*/ ) {
                    if( !proximity_timeout ) {
                        ++proximity_sense;
                        if( !set_proximity_sensitivity )
                            proximity_timeout = 5;
                    }
                }
                IRTestSum = 0;
                IRTestCount = 0;
            }

            if( !halfSec ) {
                if( gps_fixgood )
                    --gps_fixgood;
                if( gps_datetimelatlongood )
                    --gps_datetimelatlongood;
                if( gps_good_counter )
                    --gps_good_counter;
                if( proximity_timeout )
                    --proximity_timeout;
    //          If we lose GPS lock but still get time, might as well assume we're still in the same time zone.
    //          So leave these two lines commented out.
    //          if( !gps_fixgood || !gps_datetimelatlongood )
    //              timezone_found = -1;
            }

            if( digit_test_mode ) {
                lbutton_short_press = 0;
                lbutton_long_press = 0;
                rbutton_short_press = 0;
                rbutton_long_press = 0;
                bbutton_short_press = 0;
                bbutton_shortlong_press_lr = 0;
                bbutton_shortlong_press_rl = 0;
                proximity_sense = 0;
            }
            while( lbutton_short_press ) {
                if( set_nixie_brightness || set_led_brightness ) {
                    if( new_brightness > 0 ) {
                        --new_brightness;
                        immediate_update = 1;
                    }
                } else if( set_tz ) {
                    if( new_tz_override == -1 ) {
                        new_tz_override = 2345;
                    } else if( new_tz_override == 0 ) {
                        new_tz_override = -1;
                    } else {
                        new_tz_override -= 15;
                        if( new_tz_override%100 == 85 )
                            new_tz_override -= 40;
                    }
                    immediate_update = 1;
                } else if( !time_set || set_alarm ) {
                    if( set_date ) {
                        unsigned char dim;
                        switch(time_set_digit) {
                            case 0:
                                date.mday += 0x10;
                                if( (date.mday&0xF0) == 0x40 )
                                    date.mday -= 0x40;
                                else if( date.mday > dec_to_bcd(get_days_in_month(bcd_to_dec(date.mon), bcd_to_dec(date.year))) )
                                    date.mday &= 0x0F;

                                if( date.mday == 0 )
                                    date.mday = 1;
                                break;
                            case 1:
                                date.mday += 0x01;
                                if( (date.mday&0x0F) == 0x0A )
#ifdef DIGIT_SET_WRAP_AROUND
                                    date.mday -= 0x0A;
#else
                                    date.mday += 0x06;
#endif
                                if( date.mday == 0 || date.mday > dec_to_bcd(get_days_in_month(bcd_to_dec(date.mon), bcd_to_dec(date.year))) )
                                    date.mday = 1;
                                break;
                            case 2:
                                if( date.mon <= 2 )
                                    date.mon += 0x10;

                                if( date.mon >= 0x10 )
                                    date.mon -= 0x10;
                                if( date.mon == 0 )
                                    date.mon = 1;
                                break;
                            case 3:
                                date.mon += 0x01;
                                if( (date.mon&0x0F) == 0x0A )
#ifdef DIGIT_SET_WRAP_AROUND
                                    date.mon -= 0x0A;
#else
                                    date.mon += 0x06;
#endif

                                if( date.mon == 0 || date.mon > 0x12 )
                                    date.mon = 1;
                                break;
                            case 4:
                                date.year += 0x10;
                                if( (date.year&0xF0) == 0xA0 )
                                    date.year -= 0xA0;
                                break;
                            case 5:
                                date.year += 0x01;
                                if( (date.year&0x0F) == 0x0A )
#ifdef DIGIT_SET_WRAP_AROUND
                                    date.year -= 0x0A;
#else
                                    date.year += 0x06;
                                if( (date.year&0xF0) == 0xA0 )
                                    date.year -= 0xA0;
#endif
                                break;
                        }
                        dim = dec_to_bcd(get_days_in_month(bcd_to_dec(date.mon), bcd_to_dec(date.year)));
                        if( date.mday > dim )
                            date.mday = dim;
                        immediate_update = 1;
                    } else if( set_alarm && set_alarm_day ) {
                        alarm_day_mask ^= (1<<(time_set_digit + set_alarm_day - 1));
                        immediate_update = 1;
                    } else {
                        rtccTime* t = set_alarm ? &alarm_time : &time;
                        switch(time_set_digit) {
                            case 0:
                                t->hour += 0x10;
                                if( (t->hour&0xF0) == 0x30 )
                                    t->hour -= 0x30;
                                else if( t->hour >= 0x24 )
                                    t->hour -= 0x20;
                                break;
                            case 1:
                                t->hour += 0x01;
                                if( (t->hour&0x0F) == 0x0A )
#ifdef DIGIT_SET_WRAP_AROUND
                                    t->hour -= 0x0A;
#else
                                    t->hour += 0x06;
#endif
                                if( t->hour == 0x24 )
                                    t->hour = 0;
                                break;
                            case 2:
                                t->min += 0x10;
                                if( (t->min&0xF0) == 0x60 )
                                    t->min -= 0x60;
                                break;
                            case 3:
                                t->min += 0x01;
                                if( (t->min&0x0F) == 0x0A )
#ifdef DIGIT_SET_WRAP_AROUND
                                    t->min -= 0x0A;
#else
                                    t->min += 0x06;
#endif
                                if( t->min == 0x60 )
                                    t->min = 0;
                                break;
                            case 4:
                                t->sec += 0x10;
                                if( (t->sec&0xF0) == 0x60 )
                                    t->sec -= 0x60;
                                break;
                            case 5:
                                t->sec += 0x01;
                                if( (t->sec&0x0F) == 0x0A )
#ifdef DIGIT_SET_WRAP_AROUND
                                    t->sec -= 0x0A;
#else
                                    t->sec += 0x06;
#endif
                                if( t->sec == 0x60 )
                                    t->sec = 0;
                                break;
                        }
                        immediate_update = 1;
                    }
                } else if( show_alarm ) {
                    alarm_on ^= 1;
                    DataEEWrite( (crystal_trim&1023) | ((_24_hour_display&1)<<10) | ((leading_zero_blanking&1)<<11) | ((leds_enabled&1)<<12) | ((alarm_on&1)<<13) | ((alarm_day_mask&0x7F)<<14), 1 );
                    show_alarm = 10;
                } else if( set_trim ) {
                    if( crystal_trim > 0 ) {
                        --crystal_trim;
                        immediate_update = 1;
                    }
                } else if( set_proximity_sensitivity ) {
                    switch(set_proximity_sensitivity) {
                        case 1:
                            ProximityTriggerThreshold += (time_set_digit == 2 ? 100 : (time_set_digit == 1 ? 10 : 1));
                            if( ProximityTriggerThreshold > 200 )
                                ProximityTriggerThreshold -= 190;
                            break;
                        case 2:
                            ProximityModulationFrequency += (time_set_digit == 2 ? 10000 : (time_set_digit == 1 ? 1000 : 100));
                            if( ProximityModulationFrequency > 50000 )
                                ProximityModulationFrequency -= 20000;
                            break;
                        case 3:
                            ProximityDutyCycle += (time_set_digit == 2 ? 100 : (time_set_digit == 1 ? 10 : 1));
                            if( ProximityDutyCycle > 99 ) {
                                ProximityDutyCycle -= 100;
                                if( ProximityDutyCycle < 1 )
                                    ProximityDutyCycle = 1;
                            }
                            break;
                    }
                    InitIRLED(ProximityModulationFrequency, ProximityDutyCycle);
                } else if( alarm_timer ) {
                    alarm_timer = 0;
                    cur_alarm_time.l = alarm_time.l;
                } else {
                    if( show_date )
                        show_date = 0;
                    else
                        show_date = 10;
                }
                --lbutton_short_press;
            }
            while( rbutton_short_press ) {
                if( set_nixie_brightness || set_led_brightness ) {
                    if( new_brightness < 20 ) {
                        ++new_brightness;
                        immediate_update = 1;
                    }
                } else if( set_tz ) {
                    if( new_tz_override == -1 ) {
                        new_tz_override = 0;
                    } else {
                        new_tz_override += 15;
                        if( new_tz_override%100 == 60 )
                            new_tz_override += 40;
                        if( new_tz_override == 2400 )
                            new_tz_override = -1;
                    }
                    immediate_update = 1;
                } else if( !time_set || set_alarm ) {
                    if( set_alarm && set_alarm_day == 1 ) {
                        set_alarm_day = 2;
                        immediate_update = 1;
                    } else if( ++time_set_digit > 5 ) {
                        time_set_digit = 0;
                        if( set_alarm_day == 2 ) {
                            set_alarm_day = 1;
                            immediate_update = 1;
                        }
                    }
                } else if( set_trim ) {
                    if( crystal_trim < 999 ) {
                        ++crystal_trim;
                        immediate_update = 1;
                    }
                } else if( set_proximity_sensitivity ) {
                    if( ++time_set_digit == 3 )
                        time_set_digit = 0;
                } else if( alarm_timer ) {
                    alarm_timer = 0;
                    cur_alarm_time.l = alarm_time.l;
                } else {
                    show_alarm = 10;
                }
                --rbutton_short_press;
            }
            while( bbutton_shortlong_press_rl ) {
                if( !set_nixie_brightness && !set_led_brightness && !set_tz && time_set && !set_alarm && !set_trim && !set_proximity_sensitivity ) {
                    set_nixie_brightness = 1;
                    new_brightness = nixie_brightness;
                    immediate_update = 1;
                }
                --bbutton_shortlong_press_rl;
            }
            while( bbutton_shortlong_press_lr ) {
                if( !set_nixie_brightness && !set_led_brightness && !set_tz && time_set && !set_alarm && !set_trim && !set_proximity_sensitivity ) {
                    set_led_brightness = 1;
                    new_brightness = nixie_brightness;
                    immediate_update = 1;
                }
                --bbutton_shortlong_press_lr;
            }
            while( lbutton_long_press ) {
                if( set_nixie_brightness ) {
                    nixie_brightness = new_brightness;
                    DataEEWrite( nixie_brightness, 4 );
                    set_nixie_brightness = 0;
                } else if( set_led_brightness ) {
                    led_brightness = new_brightness;
                    DataEEWrite( led_brightness, 5 );
                    set_led_brightness = 0;
                } else if( set_tz ) {
                    tz_override = new_tz_override;
                    DataEEWrite( tz_override, 3 );
                    set_tz = 0;
                } else if( !time_set ) {
                    date.wday = get_dow(bcd_to_dec(date.mday), bcd_to_dec(date.mon), bcd_to_dec(date.year));
                    RtccSetTimeDate(time.l, date.l);
                    halfSec = 0;
                    time_set = 1;
                } else if( set_alarm ) {
                    alarm_on = 1;
                    cur_alarm_time.l = alarm_time.l;
                    set_alarm = 0;
                    DataEEWrite( (crystal_trim&1023) | ((_24_hour_display&1)<<10) | ((leading_zero_blanking&1)<<11) | ((leds_enabled&1)<<12) | ((alarm_on&1)<<13) | ((alarm_day_mask&0x7F)<<14), 1 );
                    DataEEWrite( alarm_time.l, 2 );
                } else if( set_trim ) {
                    RtccSetCalibration(crystal_trim-500);
                    DataEEWrite( (crystal_trim&1023) | ((_24_hour_display&1)<<10) | ((leading_zero_blanking&1)<<11) | ((leds_enabled&1)<<12) | ((alarm_on&1)<<13) | ((alarm_day_mask&0x7F)<<14), 1 );
                    set_trim = 0;
                } else if( set_proximity_sensitivity ) {
                    DataEEWrite( (ProximityModulationFrequency-30000)/100 + (ProximityDutyCycle<<8) + (ProximityTriggerThreshold<<16), 6 );
                    InitIRLED(ProximityModulationFrequency, ProximityDutyCycle);
                    set_proximity_sensitivity = 0;
                } else if( gps_datetimelatlongood || timezone_found != -1 ) {
                    set_tz = 1;
                    new_tz_override = tz_override;
                } else {
                    time_set = 0;
                    date.l = RtccGetDate();
                    set_date = 0;
                    time_set_digit = 0;
                }
                immediate_update = 1;
                --lbutton_long_press;
            }
            while( rbutton_long_press ) {
                if( set_nixie_brightness ) {
                    set_nixie_brightness = 0;
                } else if( set_led_brightness ) {
                    set_led_brightness = 0;
                } else if( set_tz ) {
                    set_tz = 0;
                } else if( !time_set ) {
                    set_date ^= 1;
                    time_set_digit = 0;
                } else if( set_alarm ) {
                    if( set_alarm_day == 0 )
                        set_alarm_day = 2;
                    else
                        set_alarm_day = 0;
                    time_set_digit = 0;
                } else if( set_trim ) {
                    crystal_trim = old_crystal_trim;
                    set_trim = 0;
                } else if( set_proximity_sensitivity ) {
                    time_set_digit = 0;
                    if( ++set_proximity_sensitivity == 4 )
                        set_proximity_sensitivity = 1;
                } else {
                    set_alarm = 1;
                    time_set_digit = 0;
                    set_alarm_day = 0;
                }
                immediate_update = 1;
                --rbutton_long_press;
            }
            while( bbutton_short_press ) {
                leds_enabled ^= 1;
                DataEEWrite( (crystal_trim&1023) | ((_24_hour_display&1)<<10) | ((leading_zero_blanking&1)<<11) | ((leds_enabled&1)<<12) | ((alarm_on&1)<<13) | ((alarm_day_mask&0x7F)<<14), 1 );
                --bbutton_short_press;
            }
            while( bbutton_long_press ) {
                if( set_nixie_brightness || set_led_brightness ) {
                    // do nothing
                } else if( !time_set || set_tz ) {
                    _24_hour_display ^= 1;
                    DataEEWrite( (crystal_trim&1023) | ((_24_hour_display&1)<<10) | ((leading_zero_blanking&1)<<11) | ((leds_enabled&1)<<12) | ((alarm_on&1)<<13) | ((alarm_day_mask&0x7F)<<14), 1 );
                    _24_hour_show_timer = 6;
                    immediate_update = 1;
                } else if( set_alarm ) {
                    set_alarm = 0;
                    set_proximity_sensitivity = 1;
                    proximity_trigger = 0;
                    time_set_digit = 0;
                } else {
                    if( !set_trim ) {
                        old_crystal_trim = crystal_trim;
                        set_trim = 1;
                    } else {
                        crystal_trim = old_crystal_trim;
                        set_trim = 0;
                        leading_zero_blanking ^= 1;
                        DataEEWrite( (crystal_trim&1023) | ((_24_hour_display&1)<<10) | ((leading_zero_blanking&1)<<11) | ((leds_enabled&1)<<12) | ((alarm_on&1)<<13) | ((alarm_day_mask&0x7F)<<14), 1 );
                    }
                    immediate_update = 1;
                }
                --bbutton_long_press;
            }
            while( proximity_sense ) {
                if( alarm_timer ) {
                    alarm_timer = 0;
                    cur_alarm_time.min += 0x10;
    //                if( (cur_alarm_time.min&15) >= 10 )
    //                    cur_alarm_time.min += 6;
                    if( cur_alarm_time.min >= 0x60 ) {
                        cur_alarm_time.min -= 0x60;
                        cur_alarm_time.hour += 1;
                        if( (cur_alarm_time.hour&15) == 10 )
                            cur_alarm_time.hour += 6;
                        if( (cur_alarm_time.hour&15) == 0x24 )
                            cur_alarm_time.hour = 0;
                    }
                } else {
                    if( set_proximity_sensitivity )
                        proximity_trigger = 1;
                    else if( !set_nixie_brightness && !set_led_brightness && !set_tz && time_set && !set_alarm && !set_date && !show_alarm && !set_trim && !show_date )
                        show_date = 10;
                }
                immediate_update = 1;
                --proximity_sense;
            }
            last_time.l = time.l;
            last_halfSec = halfSec;

            if( alarm_on && time.hour == cur_alarm_time.hour && time.min == cur_alarm_time.min && time.sec == cur_alarm_time.sec ) {
                date.l = RtccGetDate();
                if( alarm_day_mask & (1<<date.wday) )
                    alarm_timer = 60;
            } else if( !halfSec && alarm_timer ) {
                if( --alarm_timer == 0 )
                    cur_alarm_time.l = alarm_time.l;
            }

            if( nixie_brightness == 0 ) {
                memset(brightness, MAX_BRIGHT, 6);
            } else {
                int bright_val = ldr_val * MAX_BRIGHT / 8 / nixie_brightness;
                if( bright_val > MAX_BRIGHT )
                    bright_val = MAX_BRIGHT;
                else if( bright_val < MAX_BRIGHT/8 )
                    bright_val = MAX_BRIGHT/8;
                memset(brightness, bright_val, 6);
            }
            if( led_brightness == 0 ) {
                led_bright = MAX_BRIGHT;
            } else {
                int bright_val = ldr_val * MAX_BRIGHT / 8 / led_brightness;
                if( bright_val > MAX_BRIGHT )
                    bright_val = MAX_BRIGHT;
                else if( bright_val < 0 )
                    bright_val = 0;
                led_bright = bright_val;
            }

            if( digit_test_mode ) {
                display[5] = '0' + digit_test_mode - 1;
                display[4] = '0' + digit_test_mode - 1;
                display[3] = '0' + digit_test_mode - 1;
                display[2] = '0' + digit_test_mode - 1;
                display[1] = '0' + digit_test_mode - 1;
                display[0] = '0' + digit_test_mode - 1;
                if( ++digit_test_mode > 10 )
                    digit_test_mode = 0;
            } else if( _24_hour_show_timer ) {
                if( _24_hour_display )
                    strcpy(display, "  24  ");
                else
                    strcpy(display, "  12  ");
            } else if( set_tz ) {
                if( new_tz_override < 0 ) {
                    display[5] = ' ';
                    display[4] = ' ';
                    display[3] = '0';
                    display[2] = '0';
                    display[1] = ' ';
                    display[0] = ' ';
                } else {
                    display[5] = '0';
                    display[4] = '0';
                    display[3] = '0' + (new_tz_override%10);
                    display[2] = '0' + ((new_tz_override/10)%10);
                    display[1] = '0' + ((new_tz_override/100)%10);
                    display[0] = '0' + (new_tz_override/1000);
                }
            } else if( set_nixie_brightness || set_led_brightness ) {
                display[5] = '0' + (new_brightness%10);
                display[4] = '0' + (new_brightness/10);
                display[3] = ' ';
                display[2] = ' ';
                display[1] = ' ';
                display[0] = ' ';
            } else if( show_date || (!time_set && set_date) ) {
                display[5] = '0' + (date.year&15);
                display[4] = '0' + (date.year>>4);
                display[3] = '0' + (date.mon&15);
                display[2] = '0' + (date.mon>>4);
                display[1] = '0' + (date.mday&15);
                display[0] = '0' + (date.mday>>4);
                if( show_date )
                    --show_date;
            } else if( set_alarm || show_alarm ) {
                if( set_alarm && set_alarm_day ) {
                    if( set_alarm_day == 2 ) {
                        display[0] = ((alarm_day_mask & (1<<1)) || time_set_digit == 0) ? '1' : ' ';
                        display[1] = ((alarm_day_mask & (1<<2)) || time_set_digit == 1) ? '2' : ' ';
                        display[2] = ((alarm_day_mask & (1<<3)) || time_set_digit == 2) ? '3' : ' ';
                        display[3] = ((alarm_day_mask & (1<<4)) || time_set_digit == 3) ? '4' : ' ';
                        display[4] = ((alarm_day_mask & (1<<5)) || time_set_digit == 4) ? '5' : ' ';
                        display[5] = ((alarm_day_mask & (1<<6)) || time_set_digit == 5) ? '6' : ' ';
                    } else {
                        display[0] = ((alarm_day_mask & (1<<0)) || time_set_digit == 0) ? '0' : ' ';
                        display[1] = ((alarm_day_mask & (1<<1)) || time_set_digit == 1) ? '1' : ' ';
                        display[2] = ((alarm_day_mask & (1<<2)) || time_set_digit == 2) ? '2' : ' ';
                        display[3] = ((alarm_day_mask & (1<<3)) || time_set_digit == 3) ? '3' : ' ';
                        display[4] = ((alarm_day_mask & (1<<4)) || time_set_digit == 4) ? '4' : ' ';
                        display[5] = ((alarm_day_mask & (1<<5)) || time_set_digit == 5) ? '5' : ' ';
                    }
                    if( !(alarm_day_mask & (1<<(set_alarm_day + time_set_digit - 1))) ) {
                        if( halfSec ) {
                            if( !immediate_update )
                                display[time_set_digit] = ' ';
                        } else {
                            brightness[time_set_digit] >>= 1;
                        }
                    } else if( halfSec ) {
                        brightness[time_set_digit] >>= 2;
                        if( brightness[time_set_digit] < (MAX_BRIGHT/4) )
                            brightness[time_set_digit] = (MAX_BRIGHT/4);
                    }
                } else {
                    display[5] = '0' + (alarm_time.sec&15);
                    display[4] = '0' + (alarm_time.sec>>4);
                    display[3] = '0' + (alarm_time.min&15);
                    display[2] = '0' + (alarm_time.min>>4);
                    display[1] = '0' + (alarm_time.hour&15);
                    display[0] = '0' + (alarm_time.hour>>4);
                    if( show_alarm && leading_zero_blanking && display[0] == '0' )
                        display[0] = ' ';
                }
            } else if( set_trim ) {
                sprintf(display, "  %3d ", crystal_trim);
                display[5] = display[4];
                display[4] = ' ';
            } else if( set_proximity_sensitivity ) {
                display[0] = '0' + set_proximity_sensitivity;
                display[1] = proximity_trigger ? '1' : ' ';
    //            display[2] = ' ';
    //            display[3] = ' ';
                display[4] = ' ';
    //            display[5] = ' ';
                switch(set_proximity_sensitivity) {
                    case 1:
                        display[2] = '0' + (ProximityTriggerThreshold/100);
                        display[3] = '0' + ((ProximityTriggerThreshold/10)%10);
                        display[5] = '0' + (ProximityTriggerThreshold%10);
                        break;
                    case 2:
                        display[2] = '0' + (ProximityModulationFrequency/10000);
                        display[3] = '0' + ((ProximityModulationFrequency/1000)%10);
                        display[5] = '0' + ((ProximityModulationFrequency/100)%10);
                        break;
                    case 3:
                        display[2] = '0' + (ProximityDutyCycle/100);
                        display[3] = '0' + ((ProximityDutyCycle/10)%10);
                        display[5] = '0' + (ProximityDutyCycle%10);
                        break;
                }
                if( halfSec ) {
                    switch(time_set_digit) {
                        case 0:
                            display[5] = ' ';
                            break;
                        case 1:
                            display[3] = ' ';
                            break;
                        case 2:
                            display[2] = ' ';
                            break;
                    }
                }
                proximity_trigger = 0;
            } else {
                display[5] = '0' + (time.sec&15);
                display[4] = '0' + (time.sec>>4);
                display[3] = '0' + (time.min&15);
                display[2] = '0' + (time.min>>4);
                if( _24_hour_display || !time_set || (time.hour > 0 && time.hour <= 0x12) ) {
                    display[1] = '0' + (time.hour&15);
                    display[0] = '0' + (time.hour>>4);
                } else {
                    unsigned char hour = time.hour;
                    if( hour == 0 )
                        hour = 0x12;
                    else
                        hour = dec_to_bcd(bcd_to_dec(time.hour) - 12);
                    display[1] = '0' + (hour&15);
                    display[0] = '0' + (hour>>4);
                }
                if( time_set && leading_zero_blanking && display[0] == '0' )
                    display[0] = ' ';
            }
            if( digit_test_mode ) {
                buzzer_on = display[0] == '0';
                leds_on = (digit_test_mode&1);
                memset(brightness, MAX_BRIGHT, 6);
                led_bright = MAX_BRIGHT;
            } else {
                if( (!time_set || (set_alarm && !set_alarm_day)) && !_24_hour_show_timer && halfSec ) {
                    if( !immediate_update )
                        display[time_set_digit] = ' ';
                } else if( ((show_alarm && !alarm_on) || alarm_timer || _24_hour_show_timer || set_tz || set_nixie_brightness || set_led_brightness) && halfSec ) {
                    memset(display, ' ', 6);
                } else if( set_trim && halfSec ) {
                    display[2] = ' ';
                }
                if( _24_hour_show_timer )
                    --_24_hour_show_timer;
                if( show_alarm )
                    --show_alarm;

                if( alarm_timer ) {
                    buzzer_on = !halfSec;
                    leds_on = !halfSec;
                    memset(brightness, MAX_BRIGHT, 6);
                } else {
                    buzzer_on = 0;
                    leds_on = leds_enabled;
                }
            }
            if( gps_detected && !gps_good_counter && halfSec ) {
                int i;
                for( i = 0; i < 6; ++i )
                    brightness[i] = brightness[i] * 2 / 3;
            }

            UpdateOutputs(TMR1%MAX_BRIGHT);
            immediate_update = 0;
    #ifdef DEBUG_MODE
            {
                char temp[32];
                sprintf(temp, "%s%c%c[%s]%c", display, buzzer_on ? '!' : ' ', leds_on ? '*' : ' ', brightness, gps_fixgood && gps_datetimelatlongood ? (timezone_found != -1 ? '0' + timezone_found : '!') : (gps_fixgood|gps_datetimelatlongood ? '?' : '.'));
                Debug_SendSerialString(temp);
            }
    #endif

            if( !halfSec ) {
                int i;

                ldr_vals[cur_ldr_val] = 1023 - measure_LDR();
                cur_ldr_val = (cur_ldr_val + 1) & (sizeof(ldr_vals) / sizeof(*ldr_vals) - 1);
                if( num_ldr_vals < (sizeof(ldr_vals) / sizeof(*ldr_vals)) )
                    ++num_ldr_vals;
                ldr_val = 0;
                for( i = 0; i < num_ldr_vals; ++i )
                    ldr_val += ldr_vals[i];
                if( ldr_val > 24 )
                    ldr_val -= 24;
                else
                    ldr_val = 0;
                ldr_val /= num_ldr_vals;

                val = check_supply_voltage();
    //          sprintf(string, "v%d", val);
    //          Debug_SendSerialString(string);
                if( val < 250/*192*/ ) {
                    unsigned char inverted = U1MODEbits.RXINV;
                    unsigned int baud_rate = 500000 / ((unsigned int)U1BRG + 1);

                    memset(display, ' ', 6);
                    UpdateOutputs(0);
                    if( IRTesting )
                        FinishIRTest();
    #ifdef DEBUG_MODE
                    Debug_SendSerialString("sleep");
    #endif
                    close_serial();
                    do {
                        LowPowerSleep();
                    } while( check_supply_voltage() < 270 );

                    if( OSCCONbits.COSC != 7 ) {
                        unsigned int status1, status2;
                        mSYSTEMUnlock(status1, status2);
                        OSCCONbits.NOSC = 7; // switch back to FRC
                        OSCCONbits.OSWEN = 1;
                        mSYSTEMLock(status1, status2);
                        while( OSCCONbits.OSWEN )
                            ;
                    }
                    setup_serial(inverted, baud_rate > 5000 ? 9600 : 4800);
                    break; // reinitialise, just to be safe
                }
            }
        }
    }
    return (EXIT_SUCCESS);
}
