#include <18F1320.h>
#case

#include <stdlib.h>

#use delay(clock=8000000)
#use rs232(baud=19200,parity=N,xmit=PIN_B1,rcv=PIN_B4,bits=8)

#define DS1820_DQ PIN_B3
#include "ds1820.c"

// B3 - DQ of DS1820
// B2 - ALARM#
// B5 - LOAD#

static int1 alarm_is_on;
#define ALARM_ON()  { alarm_is_on = 1; output_low(PIN_B2); }
#define ALARM_OFF() { alarm_is_on = 0; output_high(PIN_B2); }

static int1 heater_is_on;
#define HEATER_ON()  { heater_is_on = 1; output_low(PIN_B5); }
#define HEATER_OFF() { heater_is_on = 0; output_high(PIN_B5); }

struct { unsigned char sec, min, hour; } t;

static signed int16 ambient_temp;
#define ALARM_AFTERNFAILURES 10

enum { MODE_OFF, MODE_ON, MODE_SCHEDULED, MODE_AWAY } mode;

#define NSCHEDULE 3
struct settings_t {
   signed int16 home_temp_min;
   signed int16 home_temp_max;
   signed int16 away_temp_min;
   signed int16 away_temp_max;

   struct {
      unsigned char on_h, on_m, off_h, off_m;
   } schedule[NSCHEDULE];
};

struct settings_t settings;

#int_TIMER1
isr_timer1() {
   // Timer1 ticks 32.768/8 = 4.096kHz, overflows 1Hz
   set_timer1(65536 - 4096);

   t.sec++;
   if (t.sec >= 60) {
      t.min++;
      t.sec = 0;

      if (t.min >= 60) {
         t.hour++;
         t.min = 0;

         if (t.hour >= 24) {
            t.hour = 0;
         }
      }
   }
}

void
beep_error() {
   ALARM_ON();
   delay_ms(300);
   ALARM_OFF();

   delay_ms(300);

   if (alarm_is_on) ALARM_ON();
}

void
beep_ok() {
   ALARM_ON();
   delay_ms(100);
   ALARM_OFF();

   delay_ms(100);

   ALARM_ON();
   delay_ms(100);
   ALARM_OFF();

   delay_ms(100);

   if (alarm_is_on) ALARM_ON();
}

#define GET_HMS(h, m, s) { \
   disable_interrupts(INT_TIMER1); \
   h = t.hour; m = t.min; s = t.sec; \
   enable_interrupts(INT_TIMER1); \
}

#define GET_HM(h, m) { \
   disable_interrupts(INT_TIMER1); \
   h = t.hour; m = t.min; \
   enable_interrupts(INT_TIMER1); \
}

#define GET_M(m) { \
   disable_interrupts(INT_TIMER1); \
   m = t.min; \
   enable_interrupts(INT_TIMER1); \
}

#define SET_HM(h, m) { \
   disable_interrupts(INT_TIMER1); \
   t.hour = h; t.min = m; t.sec = 0; \
   enable_interrupts(INT_TIMER1); \
}

void
reset_settings(void) {
   unsigned int i;

   // Reset everything to defaults
   mode = MODE_OFF;

   SET_HM(0, 0);
   ALARM_OFF();
   HEATER_OFF();

   ambient_temp = 255;

   settings.home_temp_min = settings.home_temp_max = 0;
   settings.away_temp_min = settings.away_temp_max = 0;

   for(i=0; i<NSCHEDULE; i++) {
      settings.schedule[i].on_h = settings.schedule[i].on_m = 0;
      settings.schedule[i].off_h = settings.schedule[i].off_m = 0;
   }
}

void
save_settings(void) {
   unsigned int i;

   for(i=0; i<sizeof(settings); i++) {
      write_eeprom(i+1, ((char*)&settings)[i]);
   }
   write_eeprom(0, PROBE_CHECKSUM(&settings, sizeof(settings)));
}

void
read_settings() {
   unsigned int i;
   struct settings_t settings1;

   for(i=0; i<sizeof(settings); i++) {
      ((char*)&settings1)[i] = read_eeprom(i+1);
   }

   if (PROBE_CHECKSUM(&settings1, sizeof(settings1)) == read_eeprom(0)) {
      settings = settings1;
      mode = MODE_AWAY;
   } else {
      printf("*** EEPROM CRC is invalid ***\r\n");
   }
}

// reads a single digit from the console
// beeps if not a digit or digit is out of range
unsigned char
read_digit(int min, int max) {
   for(;;) {
      unsigned char x;
      char ch;
      ch = getc();
      if ((ch >= '0') && (ch <= '9')) {
         x = ch - '0';
         if ((x >= min) && (x <= max)) {
            putc(ch);
            return x;
         }
      }
      beep_error();
   }
}

#define READ_HHMM(h, m) { \
   (h) = read_digit(0, 2) * 10; \
   (h) += read_digit(0, ((h) == 20) ? 3 : 9); \
   (m) = read_digit(0, 5) * 10 + read_digit(0, 9); \
}

void
display_main_menu(void) {
   unsigned int h, m, s;

   printf("\r\n");
   switch(mode) {
   case MODE_OFF:       printf("OFF  "); break;
   case MODE_ON:        printf("ON   "); break;
   case MODE_SCHEDULED: printf("SCHED"); break;
   case MODE_AWAY:      printf("AWAY "); break;
   }

   GET_HMS(h, m, s);

   printf(
      " %3ld %02u:%02u:%02u\r\n"
      "1. ON/SCHED/AWAY/OFF\r\n"
      "2. Set schedule\r\n"
      "3. Set HOME temp\r\n"
      "4. Set AWAY temp\r\n"
      "5. Set clock\r\n"
      "6. Clear alarm\r\n",
      ambient_temp, h, m, s
   );
}

void
toggle_mode() {
   switch(mode) {
   case MODE_OFF:       mode = MODE_ON; break;
   case MODE_ON:        mode = MODE_SCHEDULED; break;
   case MODE_SCHEDULED: mode = MODE_AWAY; break;
   case MODE_AWAY:      mode = MODE_OFF; break;
   }
   beep_ok();
}

void
set_schedule() {
   // display current schedule
   int i;

   printf("\r\nCurrent schedule:\r\n");
   for(i=0; i<NSCHEDULE; i++) {
      printf(" %u. %02u%02u %02u%02u\r\n",
         i+1,
         settings.schedule[i].on_h, settings.schedule[i].on_m,
         settings.schedule[i].off_h, settings.schedule[i].off_m);
   }
   printf("Edit entry (0 to exit): ");

   // read the choice
   i = read_digit(0, NSCHEDULE);
   printf("\r\n");

   if (i) {
      unsigned char h, m;
      printf("New ON time (HHMM): "); READ_HHMM(h, m);
      settings.schedule[i-1].on_h = h; settings.schedule[i-1].on_m = m;
      printf(" OFF time: "); READ_HHMM(h, m);
      settings.schedule[i-1].off_h = h; settings.schedule[i-1].off_m = m;
      printf("\r\n");
      save_settings();
      beep_ok();
   }
}

void
set_home_temp() {
   printf("\r\nSetting HOME temp\r\nCurrent min %u max %u\r\nNew min(00-29): ",
      settings.home_temp_min, settings.home_temp_max);
   settings.home_temp_min = read_digit(0, 2) * 10 + read_digit(0, 9);
   printf(" max(00-29): ");
   settings.home_temp_max = read_digit(0, 2) * 10 + read_digit(0, 9);
   printf("\r\n");
   save_settings();
   beep_ok();
}

void
set_away_temp() {
   printf("\r\nSetting AWAY temp\r\nCurrent min %u max %u\r\nNew min(00-29): ",
      settings.away_temp_min, settings.away_temp_max);
   settings.away_temp_min = read_digit(0, 2) * 10 + read_digit(0, 9);
   printf(" max(00-29): ");
   settings.away_temp_max = read_digit(0, 2) * 10 + read_digit(0, 9);
   printf("\r\n");
   save_settings();
   beep_ok();
}

void
set_clock(void) {
   unsigned char h, m, s;
   GET_HMS(h, m, s);
   printf("\r\nSetting clock\r\nCurrent time %02u:%02u:%02u\r\nNew time (HHMM): ",
      h,  m, s);

   READ_HHMM(h, m);
   SET_HM(h, m);
   printf("\r\n");
   beep_ok();
}

void
update_ambient_temp(void) {
   static unsigned char ambient_temp_m = 255; // when the last ambient temperature measurement was taken
   static char sensor_failures = 0;
   unsigned char m;

   GET_M(m);

   if (ambient_temp_m == m) return;

   // at least one minute has passed sinse the last temperature reading
   ambient_temp_m = m;

   if (ambient_temp == 255) {
      // last reading was invalid, discard the first reading
      signed int16 dummy;
      ds1820_read_temp(&dummy);
   }

   if (ds1820_read_temp(&ambient_temp)) {
      sensor_failures = 0;
   } else {
      beep_error();

      if (sensor_failures >= ALARM_AFTERNFAILURES-1) {
         ALARM_ON();
         ambient_temp = 255;
      } else {
         sensor_failures++;
      }
   }
}

void
idle(void) {
   int1 home = 0; // what temperature to maintain - home or away
   unsigned char h, m, i;
   unsigned int16 now;

   if (mode == MODE_OFF) {
      // heater is fully OFF
      HEATER_OFF();
      return;
   }

   GET_HM(h, m);

   if (mode == MODE_SCHEDULED) {
      home = 0;

      now = (unsigned int16)h * 60 + m;

      for(i=0; i<NSCHEDULE; i++) {
         unsigned int16 on;
         unsigned int16 off;
         on = (unsigned int16)settings.schedule[i].on_h * 60 +  (unsigned int16)settings.schedule[i].on_m;
         off = (unsigned int16)settings.schedule[i].off_h * 60 +  (unsigned int16)settings.schedule[i].off_m;

         if (on < off) {
            if ((on <= now) && (now < off)) {
               home = 1;
               break;
            }
         } else if (on > off) {
            if ((on <= now) || (now < off)) {
               home = 1;
               break;
            }
         }
      }
   } else if (mode == MODE_ON) {
      home = 1;
   } else if (mode == MODE_AWAY) {
      home = 0;
   }

   update_ambient_temp();

   // termostat
   if (home) {
      if (heater_is_on) {
         if (ambient_temp >= settings.home_temp_max) HEATER_OFF();
      } else {
          if (ambient_temp < settings.home_temp_min) HEATER_ON();
      }
   } else {
      if (heater_is_on) {
         if (ambient_temp >= settings.away_temp_max) HEATER_OFF();
      } else {
         if (ambient_temp < settings.away_temp_min) HEATER_ON();
      }
   }
}

void
main() {
   setup_adc_ports(NO_ANALOGS|VSS_VDD);
   setup_adc(ADC_OFF);
   setup_wdt(WDT_OFF);
   setup_timer_0(RTCC_INTERNAL);
   setup_timer_1(T1_EXTERNAL|T1_DIV_BY_8 | 0x08);
   setup_timer_2(T2_DISABLED,0,1);
   setup_timer_3(T3_DISABLED|T3_DIV_BY_1);

   reset_settings();
   read_settings();

   enable_interrupts(INT_TIMER1);
   enable_interrupts(GLOBAL);

   // Configure port for DS1820 temperature sensor
   port_b_pullups(0b00001000);
   delay_ms(1000);
   update_ambient_temp();

   // power cycle alarm
   ALARM_ON();

   printf("\r\nReady\r\n");

   display_main_menu();

   for(;;) {
      char ch;

      if (kbhit()) {
         // serial console activity
         ch = getc();
         switch(ch) {

         // === internal commands

         case 'R':
            printf("RST"); delay_ms(500);
            reset_cpu();

         case 'h':
            HEATER_OFF();
            break;

         case 'H':
            HEATER_ON();
            break;

         case 'b':
            ALARM_OFF();
            break;

         case 'B':
            ALARM_ON();
            break;

         // === menu navigation

         case '1':
            toggle_mode();
            display_main_menu();
            break;

         case '2':
            set_schedule();
            display_main_menu();
            break;

         case '3':
            set_home_temp();
            display_main_menu();
            break;

         case '4':
            set_away_temp();
            display_main_menu();
            break;

         case '5':
            set_clock();
            display_main_menu();
            break;

         case '6':
            ALARM_OFF();
            display_main_menu();
            break;

         default:
            // unknown key pressed
            display_main_menu();
            break;
         }
      } else {
         // no keys pressed
         idle();
      }
   }
}

