arduino_art_clock_bell.ino
/*
 * 10/9/2017 = 27/9/2019
 * CLOCK BELL project
 * 
 * Maintains time, synchronized with a Real Time Clock (RTC) and 
 * rings a bell on the hour and on half hours
 * 
 * Modification so that triggered events may last not seconds but deci seconds
 * 
 * Example for ringing  hour 02:00:00 and 14:00:00 (num_bells = 2)
 *       ______________            ______________
 *       I            I            I            I
 *       I            I            I            I
 * ______I            I____________I            I____________
 * 
 *       <--bell_on--><---bell_off-><--bell_on--><--bell_off->
 *       <------------------ ring_on = true ----------------->
 *       
 *  Daylight Saving DST is still not supported     
 *       
 *  To display characters on the LCD display I use the libary     
 *  LiquidCrystal I2C version 1.2 Author Frank de Brabander
 *  Maintainer Marco Schwartz Websitehttps://github.com/marcoschwartz/LiquidCrystal_I2C
 *  which I downloaded from https://www.arduinolibraries.info/libraries/liquid-crystal-i2-c     
 *  WARNING: This library uses the pins of the PCF8574 driver differently than other libraries 
 *  I have used in the past. I had to modify my wiring between PCF8574 and the LCD display. 
 *  Currently, you can buy LCD displays which include a PCF8574 module at almost the same 
 *  price as the LCD itself. Prefer this to save a lot of wiring. If the library does not work
 *  properly, try the library LiquidCrystal_PCF8574 by Mattias Hertel
 *       http://www.mathertel.de/Arduino/LiquidCrystal_PCF8574.aspx
 *       
 *      
 *  27/2/2019 Added NightMode, meaning that if active, the bell will not ring     
 *  on late night hours (actually on selected hours, but NightMode was a catchy phrase!!).
 *  
 *  When the last character of the first line shows a Lock symbol, this means that 
 *  clock is in silent mode and will not ring on silent hours.
 *  If the bell character is shown, the bell will sound for all hours
 *  
 *  When the last character of the second line shows a music/note symbol
 *  this means that this is not a silent hour. If it is blank, it is a silent hour.
 *  
 *  
 */
 
// To use an I2C LCD 2 lines by 16 columns, at I2C address 0x38 
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
 
#define LCD_ADDRESS 0x38
 
#include "RTClib.h"  //RTClib by NeiroN
//DS1307 rtc;
DS3231 rtc;
 
DateTime now;
 
#define LED_PIN  LED_BUILTIN // LED that flashes every sec. If you want to  use another pin rather than the built-in led, set it here
#define BELL_PIN 7          // Output that controls the bell
#define BELL_HALF_PIN 7     // Output that controls the half hour bell. Can be same as hour pin
 
 
// Input switches
#define SET_HOURS_PIN 6      // Pressing the button increases hours by one
#define SET_MINUTES_PIN 5    // Pressing the button once increases minutes by one
#define SET_SECONDS_PIN 4   // Pressing the button once sets seconds to zero
 
// The name NIGHT_MODE_PIN was probably taken by some library and caused error
// Had to name it NIGHT_PIN
#define NIGHT_PIN 3  // If set to LOW, by connecting the two pin headers, night mode will be inactive
 
#define DEBOUNCING_TIME 2    // Debouncing time for reading switches in msec
 
#define CLOCK_CORRECTION 0  // msec If crystal gains or lags, we can fix the clock making it run faster (negative value) or slower (positive value)
 
/*
 * One ring is composed of several bells.
 * Each bell has a bell_on time of duration SEC_BELL_ON (in sec)
 * and a bell_off time of duration SEC_BELL_OFF (in sec)
 * 
 */
#define TIME_BELL_ON 1  // times 100 milliseconds. The max achieved time is 25,5 sec for a value of 255
#define TIME_BELL_OFF 35  // times 100 milliseconds. The max achieved time is 25,5 sec for a value of 255                         
 
// Define the silent hours, i.e. those that the bell will not ring
int silent_hours[] = { 20, 21, 22, 23, 0, 1, 2, 3, 4, 5, 6, 7 };
 
// Definition of a custom bell, speaker and sound images
// from https://lastminuteengineers.com/arduino-1602-character-lcd-tutorial/
byte Bell[8] = {
0b00100,
0b01110,
0b01110,
0b01110,
0b11111,
0b00000,
0b00100,
0b00000
};
 
byte Speaker[8] = {
0b00001,
0b00011,
0b01111,
0b01111,
0b01111,
0b00011,
0b00001,
0b00000
};
 
 
byte Sound[8] = {
0b00001,
0b00011,
0b00101,
0b01001,
0b01001,
0b01011,
0b11011,
0b11000
};
 
byte Lock[8] = {
0b01110,
0b10001,
0b10001,
0b11111,
0b11011,
0b11011,
0b11111,
0b00000
};
 
// Volatile variables because they can be used in the interrupt service routine
 
// Declares the time variables and set to initial value. 
volatile int hours = 00; 
volatile int minutes = 00;
volatile int seconds = 05;  
volatile int deci_seconds = 00;
 
// In order to set the RTC, I need those
volatile int myyear;
volatile int mymonth;
volatile int myday;
 
volatile unsigned long current_time;
volatile unsigned long previous_time = millis();
volatile unsigned long deci_previous_time = millis();
 
// Variables to represent the timing of each hit of the hammer to the bell
// Values are in seconds
volatile int t_bell_on = 0;
volatile int t_bell_off = 0;
 
// Boolean to monitor if a ringing process (several bells) is active
boolean ring_on = false;
 
// Indicates that ringing is taking place for the half hour
// Used only to select the bell pin
boolean ring_30min = false;
 
boolean bell_on = false; // Indicates that we are at the HIGH period of the bell pulse
boolean bell_off = false;// Indicates that we are at the LOW period of the bell pulse
 
int num_bells = 0;
 
int correction = CLOCK_CORRECTION;
int interval = 100 + correction;
 
boolean night_mode = true; // Indicates if night mode is set, by reading the input pin
 
LiquidCrystal_I2C lcd(LCD_ADDRESS,16,2);  
 
void setup()
{
 
 
  lcd.init();
  lcd.backlight();
 
  pinMode(LED_PIN, OUTPUT);
  pinMode(BELL_PIN, OUTPUT);
  digitalWrite(BELL_PIN, LOW);
  pinMode(BELL_HALF_PIN, OUTPUT);
  digitalWrite(BELL_HALF_PIN, LOW);  
 
  pinMode(SET_HOURS_PIN, INPUT_PULLUP);
  pinMode(SET_MINUTES_PIN, INPUT_PULLUP);
  pinMode(SET_SECONDS_PIN, INPUT_PULLUP);
 
  pinMode(NIGHT_PIN, INPUT_PULLUP);
 
  lcd.createChar(0, Bell);
  lcd.createChar(1, Speaker);
  lcd.createChar(2, Sound);
  lcd.createChar(3, Lock);
 
  sync_from_RTC();
 
  Serial.begin(9600);
 
}
 
// Runs every interval (100msec = 1ds)
void every_interval()      
{ 
  // Handle the bell timing
  // Decrement all bell timers. We do not care if they are active or not
  if (t_bell_on > 0)
    t_bell_on --;
 
  if (t_bell_off > 0)
    t_bell_off --;  
 
  deci_seconds ++;
  if (deci_seconds >= 10) {   
    deci_seconds = 0;       
    every_second();     
  }   
  return;
}
 
 
void every_second()
{
    seconds ++;        
    if (seconds >= 60) { 
      seconds = 0;    
      minutes ++;           
      if (minutes >= 60) {
        minutes = 0;
        hours ++;       
        if (hours >= 24) {
          hours = 0;
        }
      }   
    } 
   //  IT IS WRONG TO PLACE HERE show_time_on_lcd(); . IT DISTURBS TIMING
   //  IT STEELS TIME (approx 4msec) FROM TIME_BELL_ON
   //  LCD handling must be placed in loop2()       
}
 
 
// Presents the time on the LCD
void show_time_on_lcd()
{
 
  // print date on line 1 of LCD
  lcd.setCursor(2,0);
  if (myday < 10)
    lcd.print("0");  
  lcd.print(myday);
  lcd.print(".");
  if (mymonth < 10)
    lcd.print("0");
  lcd.print(mymonth);
  lcd.print(".");
  lcd.print(myyear);
 
  // print hour on line 2 of LCD   
  lcd.setCursor(3,1);
  if (hours < 10)
    lcd.print("0");
  lcd.print(hours);
  lcd.print(":");
  if (minutes < 10)
    lcd.print("0");
  lcd.print(minutes);
  lcd.print(":");
  if (seconds < 10)
    lcd.print("0");
  lcd.print(seconds); 
 
  // Prints an indication for Night Mode
  lcd.setCursor(15,0);
  if (night_mode)
    lcd.write(byte(3)); // Lock symbol
  else
    lcd.write(byte(0));  // Bell symbol
 
  // Prints an indication if the hour is in the list of silent hours
  lcd.setCursor(15,1);
  if (silent_hour(hours))
    lcd.print(" ");  
  else
    lcd.write(byte(2));   // Sound symbol 
 
  return; 
}
 
/*
 * Do not use loop() for user code, because it handles timing.
 * Use loop2() instead
 */
void loop()
{
  current_time = millis();
  if (current_time - previous_time >= interval) {
    every_interval(); 
    previous_time = current_time;  
  }
 
  // do the normal work
  loop2();   
}
 
 
 
// User code goes here
void loop2()
{
  // Synchronize with RTC every minute at xx:50:00:00
  // Close to the end of the minute to avoid interacting with 
  // actions that normally occur at xx:00:00:00
  if (seconds == 50 && deci_seconds == 0) {
    sync_from_RTC();
 
  }
 
  if (ring_on)
    process_ringing();
  else {
    if (seconds == 0  && deci_seconds == 0) {
      // Addition for night mode 
      if (minutes == 0)
         if (!night_mode)
           ring(hours);
         else  if (!silent_hour(hours))
            ring(hours);
         else {
            // be silent             
         }
 
      else {
        if (minutes == 30)
          if (!night_mode)
            ring_half();
          else  if (!silent_hour(hours))
            ring_half();
         else {
            // be silent             
         } 
 
      }     
    }        
  }
 
  // LCD handling
  static int previous_seconds;
  if (seconds != previous_seconds) {
    show_time_on_lcd();
    previous_seconds = seconds;
  }
 
  // Set time manually via switches
  read_switches();
 
  return;
}
 
void read_switches() 
{
  if (digitalRead(SET_HOURS_PIN) == LOW) {
    delay(DEBOUNCING_TIME);
    if (digitalRead(SET_HOURS_PIN) == LOW) {
      hours = (hours + 1) % 24;
    }
    while (digitalRead(SET_HOURS_PIN) == LOW){ 
    }
    set_RTC();
  }  
 
  if (digitalRead(SET_MINUTES_PIN) == LOW) {
    delay(DEBOUNCING_TIME);
    if (digitalRead(SET_MINUTES_PIN) == LOW) {
      minutes = (minutes + 1) % 60;
    }  
    while (digitalRead(SET_MINUTES_PIN) == LOW) {  
    }
    set_RTC();
  }  
 
  if (digitalRead(SET_SECONDS_PIN) == LOW) {
    delay(DEBOUNCING_TIME);
    if (digitalRead(SET_SECONDS_PIN) == LOW) {
      seconds = 00; 
      deci_seconds = 0; 
    }  
    while (digitalRead(SET_SECONDS_PIN) == LOW) {  
    }
    set_RTC();
  } 
 
  if (digitalRead(NIGHT_PIN) == LOW) {
     night_mode = false; // Headers are connected, bells ring all round the clock    
  } else {
    night_mode = true;
  }
  return;   
}
 
void set_RTC()
{
 
  rtc.adjust(DateTime(myyear, mymonth, myday, hours, minutes, seconds));
  return;
}
 
void start_bell()
{
  int bell_pin;
  if (ring_30min)
    bell_pin = BELL_HALF_PIN;
  else
    bell_pin = BELL_PIN;  
  digitalWrite(bell_pin, HIGH);
  digitalWrite(LED_PIN, HIGH);
  bell_on = true;
  bell_off = false;
  t_bell_on = TIME_BELL_ON; 
  return;
}
 
void stop_bell()
{
  int bell_pin;
  if (ring_30min)
    bell_pin = BELL_HALF_PIN;
  else
    bell_pin = BELL_PIN;  
  digitalWrite(bell_pin, LOW);
  digitalWrite(LED_PIN, LOW);
  bell_on = false;
  bell_off = true;
  t_bell_off = TIME_BELL_OFF; 
  return;
}
 
/* Rings the bell, depending on the hour provided.
   The hours variable works on the 24 hour format
   but we want to ring the bell as per the 12 hour
 
   Skips ringing if previous hours is same.
   This condition may happen if we have a ring and after some time,
   the RTC synchronizes the internal clock and we go back to xx:00:00
   This functionality is only for full hours. Half hours and 01:00:00 are 
   totally separated
 
*/ 
void ring(int num_hours)
{
  static int previous_hours = 0;
 
  // protect from setting back hours via switches or sync with RTC
  if (num_hours == previous_hours) 
    return;
 
  previous_hours = num_hours;  
  ring_on = true;
  if (num_hours > 12)
    num_bells = num_hours - 12;
  else if (num_hours == 0) 
    num_bells = 12;
  else  
    num_bells = num_hours;
  start_bell();  
  return;  
}
 
// Ring at half hour
void ring_half()
{
  // Is there a way to protect from syncing back to xx:30:00 ????
 
  ring_30min = true; 
  ring_on = true;
  num_bells = 1;
  start_bell();
  return;
}
 
void process_ringing()
{
  if (bell_on && t_bell_on <= 0)
    stop_bell();
 
  if (bell_off && t_bell_off <= 0) {
    num_bells --;
    if (num_bells > 0)
      start_bell();
    else {
      bell_off = false;
      ring_on = false; 
      ring_30min = false;   
    }
  }
  return;
}
 
void send_time_to_serial()
{
  Serial.print("Time: "); 
  if (hours , 10)
    Serial.print("0");
  Serial.print(hours);
  Serial.print(":");
  if (minutes < 10)
    Serial.print("0");
  Serial.print(minutes);
  Serial.print(":");
  if (seconds < 10)
    Serial.print("0");
  Serial.println(seconds);  
}
 
// Presents variables on LCD for debugging
void show_state_on_lcd()
{
  // Clear line 1
  lcd.setCursor(0,1);
  lcd.print("                ");
 
  lcd.setCursor(0,1);
  lcd.print("R:");
    if (ring_on) {
      lcd.print("ON");
      lcd.print(" B=");
      lcd.print(num_bells);
      lcd.print(" TB=");
      lcd.print(t_bell_on);
      lcd.print(" TBO=");
      lcd.print(t_bell_off);      
 
    }  
    else
      lcd.print("OFF");
 
 
}
 
void sync_from_RTC()
{
  now = rtc.now();
 
  myyear = now.year();
  mymonth = now.month();
  myday = now.day();
  hours = now.hour();
  minutes = now.minute();
  seconds = now.second();  
}
 
// checks if an int value is included in an array of ints
boolean in_int_array(int value, const int *arr, int num_elements) {
 
  for (int i=0; i< num_elements ; ++i ) {
    if (value == arr[i])
      return true;       
  }
  return false;  
}
 
// check if a given hour is silent
boolean silent_hour(int this_hour) {
 
  if (in_int_array(this_hour, silent_hours, sizeof(silent_hours)/sizeof(int)))
    return true;
  else
    return false;  
}