Event Countdown Clock (Covid Clock - V2.0)

by gm310509 in Circuits > Arduino

1747 Views, 11 Favorites, 0 Comments

Event Countdown Clock (Covid Clock - V2.0)

CovidClockV02-img01-small.jpg

This is an upgrade to my earlier version of this project. I debated with myself whether I should just update the old project or post this new one. Obviously I eventually decided to post this as an "Upgrade". I decided this because while working on this project (which I built from scratch), I felt that the wiring was sufficiently complicated and tedious that the original project was a good stepping stone to get working, then it could fairly easily be modified to create this enhanced version.

However, if you wish to tackle this project from scratch, the complete wiring diagram (and a schematic) are included in Step 2. Feel free to jump right in and hook it up as you wish.

The original project can be seen here. This instructable is presented as an "add-on" to the original Coronavirus Quarantine Clock. The code in this instructable won't work as is with the V1 project. The main reason for this is that the signalling used to manage the 7 segment LED display has been inverted due to the inclusion of the transistors.

So, what is new?

  • The LED display is dimmable (see note below).
  • The code has been modularised.
  • There are additional commands.
  • The target date is stored in EEPROM so that it will be retained across restarts.

Regarding the dimmable display. There are loads of projects online that allow LEDs to be dimmed. So what is so special about this?

The LED dimming is special because it uses a "home grown" digital potentiometer. Most LED dimming projects online use PWM to dim LEDs, so why not do that? PWM works fine for dimming the LED bar graph, but since the clock display is updated / managed with strobing (due to the shared pins on the panel), applying PWM to the clock display resulted in a rather wierd looking "rolling" or "scrolling" effect. This effect would vary depending on the PWM rate.

The other interesting thing about the LED dimming is that it shows an alternative way to achieve dimming of LEDs for those situations when PWM is not desirable.


If you find my projects helpful, please consider supporting me by buying me a coffee.


Supplies

In addition to the parts in the original Coronavirus Countdown Clock, you will need the following:

  • (Optional) another breadboard. I found that I couldn't easily fit all of the components on one medium sized (830 pin) breadboard.
  • 8 x NPN transistors (I used BC337, but any similar NPN transistor will be OK).
  • 8 x 10 K resistors.
  • 4 x 50 ohm resistors (I used 100 ohm, but feel that these were too "strong" and 100 ohm was the smallest that I had).
  • 1 x LDR with about 48-140K range (but anything close is OK).
  • 1 x 2K2 resistor for use with the LDR (again, anything close is OK, but you probably don't want to go any smaller than 2K2).

Wiring Preparations From V1 Project

Quarantine Clock_V1_bb_annotated.png

As shown in the breadboard diagram, there are only two modifications to the V1 project to prepare your circuit for the additions for V2.

These are:

  1. Remove red and black wires bridging the top +V and GND bus to the lower bus on your breadboard.
    The lower bus will be used to collect the cathodes of the various LEDs and feed them into the brightness controller
  2. Seperate the 4 wires that select the digits into the LED panel into two parts. These are the four wires with the white bands in them.
    We still need both sides, but we will insert 4 of the transistors between the Arduino end and the LED panel end.

Add the V2 Connections

covidClockV2-bb-annotated.png
covidClockV2-Schematic.png

The diagrams above show the complete circuit for this project.

Note that in the diagram there are 3 seperate "power" lines. These are:

  • GND - which uses black wires and connects to one of the Arduino GND pins
  • +5V - which uses red wires and connects to the Arduino's +5V pin.
  • GNDA - which uses solid blue wires and connects the cathodes of the LEDs to the brightness control circuit. This wire does not connect directly to the black GND wire (nor the red +5 V wire).

LED Digit Control

This sub-step connects up the transistors for the digit selection on the 7 segment LED display panel.

  • Find some space on your breaboard and insert four of the transistors.
  • Connect one 10 K resistor (four in total) to each of the transistors' base pins.
    The base pin is the middle one.
  • Connect the other side of the 10K resistor to the original DIO pins on the Arduino (i.e. pins 3, 4, 5 and 6). This is the "Arduino side" of the banded wire that we "split" in the previous step.
  • Connect the other side of the "split" banded wire (i.e. the end coming from the LED panel) to the collector of each of the transistors.
    • The collector is the pin on the left if you hold the transistor so that the flat side is facing you and the pins are pointing downwards.
    • The order is important here. As per the breadboard diagram, connect the two ends of the each wire of the some colouring to the same transistor. For example DIO pin 3 still must connect to pin 1 on the LED panel (the light blue/cyan wire) it just has a transistor in the middle of it now. The same applies for the other 3 wires.
  • Connect the emitter of each of the transistors to the "GND" bus on the lower edge of the breadboard - this is shown as the solid blue wires on the breadboard diagram and GNDA on the circuit diagram.
    • The emitter is the pin on the right if you hold the transistor so that the flat side is facing you and the pins are pointing downwards. It should be the only remaining unused pin on the transistor at this point.

Ambient Light Level Sensor

This substep connects the LDR. The LDR is used to measure the ambient light levels. The brightness of all LEDs is adjusted based upon readings from this sensor.

As such, physical placement on the breadboard is somewhat important. If you place it behind or under a bunch of things that cast shadows onto it then you will get artificially low levels. The LDR should be able to "see" your room.

In a subsequent step, I will explain how to adjust the code to properly adjust the brightness for your sensor. So, while being somewhat important, we can compensate if you cannot easily place the LDR to "see" your room.

  • Insert the LDR into the breadboard. I connected one side to the +5 bus on the breadboard and the other side to the "distribution" area on the breadboard.
  • Connect a lead from the "distribution" side of the LDR to A0 on your Arduino.
  • Connect the 2K2 resistor from the "distribution" side of the LDR to the GND bus adjacent to the +V bus.
    Do not conenct it to the the GNDA (blue) bus on the other side of the breadboard that we just connected the transistors to.

Digitial Potentiometer Brightness Control

This substep hooks up the digital potentiometer which is used to control the brightness of the LEDs. The brightness control works by adding resistance to the GNDA bus (which is the cathode of all of the LEDs). As the ambient light levels get lower (i.e. the room gets darker), more resistance is added. This in turn makes the LEDs darker. As the room gets brighter, resistance is removed which makes the LEDs brighter and thus easier to see.

  • Insert the four transistors.
    • Note that placement is important, specifically the spacing between them. As can be seen from the breadboard diagram a resistor will connect the collector of one transistor to the collector of the next one.
      So, you either need to place them sufficiently close together so that the resistors can reach, or use additional wiring to bridge the gaps - both are OK.
      In my setup, I found that it worked better if the "1 hole gap" between the transistors as shown in the breadboard diagram was not present (I guess my resistors were a bit shorter than fritzing's resistors).
  • Connect each of the Emitters on the transistors (the rightmost pin when the flat side is facing you) to the black GND bus on top of the breadboard
    • Do not connect this back to the Blue GNDA bus, it must be connected to the Black GND bus.
  • Connect a 10K resistor to the Base (middle) pin of each of the transistors.
  • Connect the other side of the 10K resistor to pins 8, 9, 10 and 11 on your Arduino.
    • Note that order is important. You need to connect them so that the transistor connected to DIO pin 8 is the first transistor in the chain. The transistor connected to DIO pin 9 is the second and so on.
      The first transistor in the chain is the one that will have the blue GNDA bus connected to it.
  • Connect the Collector of the first transistor (i.e. the one connected to DIO pin 8) to the "GNDA" bus (the lower GND wire) as shown by the solid blue wire.
  • Insert a 50 ohm resistor so that it connects the Collector of the first transistor to the collector of the second one.
    • Repeat this using the remaining 50 ohm resistors to connect the 2nd transistor's collector to the 3rd transistor's collector and so on.
    • The collector is the pin on the left when holding the transistor so that the flat side is facing you.
    • The last resistor will not have a transistor to connect to. Connect the "other side" of this last resistor to the Black GND (i.e. the top bus) line on your breadboard.
      Again, do not connect this to the blue GNDA bus, it must be connected to the black GND bus.

This completes the required wiring.

How does the Brightness Control work?

The operation of the brightness control is fairly straightforward. It is wired up as a digital potentiometer or digipot. This uses as series of resistors setup in a sort of "ladder" configuration.

If none of the transistors are turned on, then any current in the solid blue wire (i.e. the current from the Cathodes of the LEDs) must flow through all four resistors before reaching GND. If you use 50 ohm resistors, this would add 200 ohms to the current limiting resistance of the LEDs for a total of 670 ohms per LED. This makes them darker (i.e. less bright) due to the extra resistance.

If the first transistor is turned on, then the current can now flow through the first transistor directly to GND. In other words, by turning on the first transistor (the one connected to DIO pin 8) the current flow will bypass all of the 50 ohm resistors in the "ladder". This means that there is no additional resistance between the LED cathodes and GND - the 470 ohm current limiting resistors connected to the anodes are still in play though, it is important that these 470 ohm resistors cannot be removed from the circuit. Thus, the LED's will be at maximum brightness (as determined by the 470 ohm resistors between the Arduino DIO pins and the LED anodes).

By turning off the first transistor and turning on one of the others, we can vary the amount of additional current limiting resistance and thus the overall brightness of the LEDs. Note that it doesn't matter if "downstream" transistors are turned on or not, the current will flow through the first on transistor to GND (effectively meaning that the remaining ones are ignored).

So by turning transistors on/off we can adjust the brightness of the display by adding or removing resistance from the LED circuit.

You might want to try substituting some of the resistors for resistors with different values to adjust the light levels to your own personal preferences. However, I recommend that you do not reduce the 470 ohm resistors below 220 ohms.

The Code

ArduinoWiFiFlowChart.png
covidClockV2-ArduinoIDE.png

The code is based upon the code from the previous version. However, I have made a few enhancements. The major new features include:

  • Addition of code to support the variable brightness.
  • Use of EEPROM to store the target date used to set the LED Bar module. This allows the target date to be preserved across restarts.
  • The code has been modularised. The code consists of 5 files, all of which are required.
  • There are 2 new commands (reset and help).
  • The status command provides much more output can be used to calibrate your LED brightness control code.
  • The reset command can be used to reset the light level data accumulated for the status command to show.

As before you can get the code from my github at https://github.com/gm310509/Arduino/tree/master/Ho...

I suggest getting the code from github, but if you prefer the good old copy and paste method, here are the five files. The Arduino screen shot shows what your IDE should look like once all of the files have been added to your project. Use the little down arrow in the top right hand corner of the IDE to add files with the names shown in the screen shot and below. Each added file will be empty, you can then copy and paste the code into the IDE.

The "mainline" QuarantineClockV02.ino:

#include <EEPROM.h>
#include <Wire.h>
#include <RTClib.h>
#include "Brightness.h"
#include "ClockDisplay.h"

/*
* Quarantine Clock.
* ----------------
*
* By gm310509
* 2020-09-26
*
* A simple program built for hotel quarantine.
* Form some reason, my hotel did not have a clock. I find it very convenient to have a clock
* so that I can just glance at the current time without having to pick up and activate a handheld device such as
* a phone or tablet (and I couldn't be bothered putting on my watch every day, and I had nothing better to do while in
* quarantine).
*
* So, I made one from some parts that I had with me.
*
* I happened to have a 10 segment bar LED, so I decided to add this to show the number of days remaining in my quarantine.
* Hence this quarantine clock program was created.
*
* The program needs a lot of I/O ports (I didn't have any multiplexors or shift registers), so it can only run
* on an Arduino with a high I/O port count - such as the Arduino Mega.
* All up, it requires 20 I/O pins.
* Additionally with the current design, it assumes that at least one MCU port (port A in this
* version - DIO pins 22-29 on the Arduino Mega) is fully accessible via the Arduino's Digital I/O connectors.
*
* Version 2.01.00.00
* ==================
* Moved clock display management into a source file of its own (ClockDisplay.c/h).
* Reorganised the commands and their descriptions into a single array (i.e. an array of structure elements).
* Upgraded the checkSerial to trigger on CR, LF or both CR/LF line terminators.
* Added BS support for terminals (e.g. putty) to checkSerial.
*
* Version 2.00.00.00
* ==================
* Added Brightness control for the LED's using a resistor ladder / digital potentiometer.
* Store qdate in EEPROM to preserve it across reboots.
*
*
* Version 1.00.00.00
* ==================
* Initial Version.
*
*/
#define VERSION "2.01.00.00"


#define CHECK_TIME_INTERVAL 1000 /* Interval between RTC time checks = 1 second */
#define CHECK_LIGHT_INTERVAL 5000 /* Interval between checks of the ambient light levels */
#define LDR_PIN A0 /* The pin that the LDR is connected to for measuring ambient light levels */
#define CLOCK_COLON_DISPLAY_TIME 500 /* How long the clock colon is turned on = 500 millisecond (or 1/2 a second) */


/*
* This is the quarantine end date.
* Modify this to reflect your quarantine end date.
* Mine was initially the 9th of October 2020.
* Then another on the 26th of March 2022.
*
* Note that this date will only be used if there isn't a valid date
* stored in EEPROM. If EEPROM has a date in it, then the date used here
* will be ingored in favour of using the EEPROM date.
* The EEPROM date is set when you use the qdate command to set the
* quarantine date.
*/
//DateTime quarantineEndDate (2020, 10, 9, 0, 0, 0); // The date that quarantine will end.
DateTime quarantineEndDate (2022, 3, 20, 0, 0, 0); // The date that quarantine will end.
//DateTime quarantineEndDate (2022, 3, 26, 0, 0, 0); // The date that quarantine will end.


unsigned long checkLightLevelTO = 0; // defines when to next check the ambiend light level.
unsigned long checkTimeTO = 0; // Variable used to track the *next* time we check the RTC.
unsigned long systemUpTime = 0; // Counts the number of seconds the system has been running.

/*
* The pins that are connected to the transistors in the digital potentiometer.
* In this version, it must contain exactly four elements (i.e. four transistors.
* This equates to 5 brightness levels. There are 5 because there can be no resistor
* enabled, the first resistor enabled and so on up to the fourth resistor (i.e.
* four resistors + no resistor = 5 levels).
*/
const int varResistorPins[] = { // These pins control a series of resistors.
8, 9, 10, 11 // The resistors are used to control the brightness of
}; // the LEDs.
Brightness ledBrightness (LDR_PIN, varResistorPins);


// Definition of the number of days remaining in quarantine bar graph LED panel.
const int barGraphLed[] = { // The digital I/O pins used to control the 10 segment BAR LED.
34, 35, 36, 37, 38, // [0] is the least significant (rightmost in my design) LED used to represent 1 day left.
39, 40, 41, 42, 43 // [9] is the most significant (leftmost in my design) LED used to represent 10 days left.
};
const int BAR_GRAPH_NUM_LEDS = sizeof(barGraphLed) / sizeof (barGraphLed[0]); // Number of bar LED's as defined by barGraphLed.
// If you wanted to add more LED's (e.g. to show the full 14 days),
// it should be a simple matter of extend barGraphLed with the extra DIO pin numbers.
// NB: I haven't tested that, but the program is intended to adapt to the number
// of elements in barGraphLed to define the number of quarantine day LEDs.


RTC_DS1307 rtc; // The realtime clock interface.


/*
* Calculate the number of quarantine days remaining (including today)
* return the number of days remaining.
*/
int getQuarantineDaysRemaining(DateTime _now) {
// create a datetime for midnight on the date specified.
DateTime _today = DateTime(_now.year(), _now.month(), _now.day(), 0, 0, 0);
TimeSpan quarantineTimeRemaining = quarantineEndDate - _today; // Work out the time remaining in days.

int daysRemaining = quarantineTimeRemaining.days();
// Serial.print(F("QDays remaining: ")); Serial.print(quarantineTimeRemaining.days());
// Serial.print(F(", daysRemaining = ")); Serial.println(daysRemaining);

if (daysRemaining < 0) { // if we have passed the quarantine end date, then
daysRemaining = 0; // simply return 0 days remaining (as opposed to a negative number).
}
return daysRemaining;
}

/*
* Given a date+time, calculate the number of days remaining in quarantine.
* Update the bar graph to show that number.
*/
void setDaysRemaining(DateTime _now) {
int daysRemaining = getQuarantineDaysRemaining(_now);
for (int i = 0; i < BAR_GRAPH_NUM_LEDS; i++) {
digitalWrite(barGraphLed[i], i < daysRemaining); // if i is within the number of remaining days, then turn the bar LED on otherwise turn it off
}
Serial.print("Setting days remaining to "); Serial.println(daysRemaining);

// Serial.print("Quarantine days remaining: "); Serial.println(quarantineTimeRemaining.days() + 1);
// DateTime rnd = DateTime(2020, 10, 8, 10, 0, 0);
// TimeSpan rndTs = quarantineEndDate - rnd;
// Serial.print("Random Date diff: "); Serial.println(rndTs.days());

}


/*
* Output a formatted date/time.
*/
void printDate(DateTime dttm) {
Serial.print("DateTime: ");
Serial.print(dttm.year()); Serial.print("/"); Serial.print(dttm.month()); Serial.print("/"); Serial.print(dttm.day());
Serial.print(" ");
Serial.print(dttm.hour()); Serial.print(":");Serial.print(dttm.minute()); Serial.print(":");Serial.println(dttm.second());
}


// Variables used to track the previous second or day.
// These are used to detect if the second or day has changed and thus trigger an appropriate action (Defined below).
int prevSecond = 99;
int prevDay = 99;

/*
* Checks the Real Time Clock (RTC) time and date.
* If the time/date has changed, then update the relevant parameters including:
* - the time to display on the clock LED.
* - the quarantine days remaining
* - whether the clock ':' should be on or off (to give the once per second blinking effect).
*/
void checkTime(unsigned long _now) {
if (_now >= checkTimeTO) { // Is it time to check the RTC?
checkTimeTO = _now + CHECK_TIME_INTERVAL; // Yep, work out when to next check it.

DateTime dttm = rtc.now(); // Read the current date and time.
//printDate(dttm);
int day = dttm.day(); // Extract the components of interest.
int hour = dttm.hour();
int minute = dttm.minute();
int second = dttm.second();

if (day != prevDay) { // Have we started a new day?
prevDay = day; // Yes, so work out the remaining quarantine days.
setDaysRemaining(dttm);
}

setTime(hour, minute); // Set the time on the clock display.

if (second != prevSecond) { // Has the second changed?
prevSecond = second; // Yes, so we need to reset the timing of the blinking colon.
resetClockColonDisplayTime(CLOCK_COLON_DISPLAY_TIME);
systemUpTime++; // Count 1 second for the system up time.
}
}
}

/**
* Periodically check the ambient light and
* adjust the brightness of the LEDs accordingly.
*/
void checkLightLevels(unsigned long _now) {
if (_now >= checkLightLevelTO) { // Is it time to check the RTC?
checkLightLevelTO = _now + CHECK_LIGHT_INTERVAL; // Yep, work out when to next check it.
ledBrightness.checkLightLevels();
}
}

/**************************
* The following set of functions and variables are used
* to interact with the clock (e.g. set the quarantine date, interrogate the system status and others).
*
*/
/*
* The list of valid commands.
*/
struct CmdStruct {
char *command;
char *description;
} commands [] = {
{ "date", "Set Date (yyyy-mm-dd)"}, // Command: 0
{ "time", "Set Time (hh:mm:ss)"}, // 1
{ "qdate", "Set Quarantine End Date (yyyy-mm-dd)."}, // 2
{ "status", "Print system status"}, // 3
{ "reset", "Reset the light metrics"}, // 4
{ "help", "Display help (i.e. this message)"} // 5
};
const int NUM_CMDS = sizeof(commands) / sizeof (commands[0]);

/*
* Output a list of the commands along with there description and some generic information.
*/
void showCommands() {
Serial.println(F("Available commands:"));
for (int i = 0; i < sizeof(commands) / sizeof(commands[0]); i++) {
Serial.print(" ");
Serial.print(commands[i].command);
if (i < NUM_CMDS) {
Serial.print(": ");
Serial.println(commands[i].description);
} else {
Serial.println();
}
}
Serial.println();
Serial.println(F("The format of any parameters is shown in parenthesis."));
Serial.println(F("For example, use the following to set the time to 1:15 PM and 22 seconds:"));
Serial.println(F(" time 13:15:22"));
Serial.println(F("In most cases, you can omit the parameter and the corresponding value will be displayed."));
Serial.println(F("For example, to show the current date, simply enter the date command with no parameter."));
Serial.println();
Serial.println(F(" ** NOTE ** there is no validation on the values entered. Make sure you enter a valid date or time."));
Serial.println();
}


/*
* Given some text, attempt to extract up to 3 integers from that text.
* The integers can be delimited by any non numeric character (including '-' and '.'). Thus, negative
* numbers will not be identified. By definition, fractional numbers will be treated as two seperate numbers
* as this function only parses integers.
*
* This function is used to extract the digits from three digit structures such as a date or a time.
* It can also be used for shorter structures (e.g. a single or dual digit structure) if need be.
*
* Return: The actual number of integers found in the text is returned.
*/
int parse3integers(char * pData, int parameterValues[]) {
int pIndex = 0; // the index where the integer will be stored.

while (pIndex < 3 && *pData) {
// find the start of the next/first number.
while ((*pData < '0' || *pData > '9') && *pData) {
pData++; // Skip characters until we get a digit or end of text.
}
if (! *pData) { // If we are at the end of the text, just return what we have found so far.
return pIndex; // return the actual number of digits found.
}
// Great, we have found a digit, so convert it to a number.
int acc = 0; // Initialise an accumulator to accumalate the digit values.
while (*pData >= '0' && *pData <= '9') {
acc = acc * 10 + *pData - '0'; // for each digit, multiply the accumulator by 10 and add in the current digit's value.
pData++; // point to the next character.
}
//Serial.print("Storing "); Serial.print(acc); Serial.print(" at index "); Serial.println(pIndex);
parameterValues[pIndex++] = acc; // Store this integer and loop back for more.
}
return pIndex; // Finally return the number of digits captured.
}


// A buffer to accumulate characters received from Serial.
char buffer[80];
int bufferPtr = 0; // A "pointer" into buffer defining where the next character shall be placed.

/*
* A command has been supplied from Serial.
* Identify the command entered and if valid, execute it.
*/
void processCommand() {

if (! buffer[0]) { // Does the buffer contain any input?
return; // No, so just return.
}

Serial.println();
Serial.print(F("Command: '")); // To begin, simply echo what we have received.
Serial.print(buffer);
Serial.println(F("'"));
// check the command is valid.


int cmdNo = -1; // Next try to identify the command from the list of available commands found in *commands[]
for(int i = 0; i < NUM_CMDS; i++) {
char *res = strcasestr(&buffer[0], commands[i].command); // Does the buffer contain the command keyword?
// Serial.print("Trying: "); Serial.print(commands[i].command); Serial.print(", "); Serial.println(res == buffer ? "match" : "no match" );
if (res == buffer) { // if res is the same address as the buffer's address, then we have a command match.
cmdNo = i; // identify the matched command
break; // and terminate the loop.
}
}

if (cmdNo == -1) { // Check that we have a valid command, if not, output the help and return.
Serial.print(F("Invalid command entered: '")); Serial.print(buffer); Serial.println(F("'"));
Serial.println(F("Enter 'help' for help"));
return;
}

// Find where the command's parameter value starts
// first, find the end of the command.
char *pData = &buffer[0]; // Skip over the command text.
while (*pData != ' ' && *pData != '\t' && *pData) {
pData++;
}

// next find the end of the whitespace.
while (*pData == ' ' || *pData == '\t') {
pData++;
}

//Serial.print("Data Portion of message: "); Serial.println(pData);

// Commands can accept up to 3 numeric values (e.g. year, month and day in a date).
// So, prepare to receive those three values.
int parameterValues[3];
int parameterCount = parse3integers(pData, &parameterValues[0]);

// Finally, try to execute the command.
if (cmdNo == 0 || cmdNo == 1) { // date or time command
if (parameterCount == 3) { // did we get three numerics as parameters?
DateTime dttm = rtc.now(); // Read the current Time.
int year = dttm.year();
int month = dttm.month();
int day = dttm.day();

int hour = dttm.hour();
int minute = dttm.minute();
int second = dttm.second();
if (cmdNo == 0) { // date command - override the current date with the supplied values.
year = parameterValues[0];
month = parameterValues[1];
day = parameterValues[2];
} else { // time command - override the current time with the supplied values.
hour = parameterValues[0];
minute = parameterValues[1];
second = parameterValues[2];
}
// This line sets the RTC with an explicit date & time, for example to set
// January 21, 2014 at 3am you would call:
//rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
// Set the date and time in accordance with the supplied values.
rtc.adjust(DateTime(year, month, day, hour, minute, second));
Serial.print(F("Setting date/time: "));
Serial.print(year); Serial.print("/"); Serial.print(month); Serial.print("/"); Serial.print(day);
Serial.print(" ");
Serial.print(hour); Serial.print(":"); Serial.print(minute); Serial.print(":"); Serial.println(second);
} else { // We did not receive three integers for the date / time commands.
if (parameterCount > 0) { // If we got any parameters, then there is a validation error. Show the help.
Serial.println(F("** Failed to parse Date or Time"));
showCommands();
} else { // We got 0 parameters, so this is a request to show the current date/time.
Serial.print(F("Current date/time: ")); printDate(rtc.now());
}
}
} else if (cmdNo == 2) { // qdate command
if (parameterCount == 3) { // Did we get three parameters? If so, set the new quarantine end date.
int year = parameterValues[0];
int month = parameterValues[1];
int day = parameterValues[2];
quarantineEndDate = DateTime(year, month, day, 0, 0, 0);
Serial.println(F("Quarantine end date set to: ")); printDate(quarantineEndDate);
setDaysRemaining(rtc.now()); // Update the days remaining value and display.
// Save the date into EEPROM to preserve it across resets.
EEPROM.write(0, year - 2000);
EEPROM.write(1, month);
EEPROM.write(2, day);
EEPROM.write(3, (year - 2000 + month + day) & 0xff);
} else { // We did not get three parameters.
if (parameterCount > 0) { // Did we get any parameters? If so, there is an invalid input, so display error and help info.
Serial.println(F("** Failed to parse Date or Time"));
showCommands();
} else { // We got zero parameters, so this is a request to show the quarantine end date information.
Serial.print(F("Quarantine end date: "));
printDate(quarantineEndDate);
Serial.print(F("Days remaining: "));
Serial.println(getQuarantineDaysRemaining(rtc.now()));
}
}
} else if (cmdNo == 3) { // Status command - takes no parameters, so just output the system status.
DateTime _now = rtc.now();
Serial.println(F("Current system status"));
Serial.print(F(" Firmware Version: ")); Serial.println(F(VERSION));
Serial.print(F(" Current Date/Time: ")); printDate(_now);
Serial.print(F(" Quarantine end Date: ")); printDate(quarantineEndDate);
Serial.print(F(" Quarantine days remaining: ")); Serial.println(getQuarantineDaysRemaining(_now));
Serial.print(F(" System uptime: ")); Serial.print(systemUpTime); Serial.println(F("s"));
#if defined(USE_INTERRUPTS)
Serial.println(F(" LED Clock refresh: Interrupt driven"));
#else
Serial.println(F(" LED Clock refresh: Best effort polling"));
#endif
Serial.println(F("Light Levels:"));
Serial.print(F(" min: ")); Serial.println(ledBrightness.getMinLightLevel());
Serial.print(F(" max: ")); Serial.println(ledBrightness.getMaxLightLevel());
Serial.print(F(" now: ")); Serial.println(ledBrightness.getLightLevel());
Serial.print(F("Dimming level: ")); Serial.println(ledBrightness.getDimmingLevel());
ledBrightness.printDebugInfo();
} else if (cmdNo == 4) { // Reset command.
ledBrightness.resetLightMetrics();
} else if (cmdNo == 5) { // Help command.
showCommands();
} else { // We should not get here, but just in case, output an error.
Serial.println(F("Unexpected command entered"));
showCommands();
}
}


/*
* Check to see if there is any input on Serial.
*
* If so, accumulate the input up to a Carriage Return (CR).
* Once a CR has been received the input is processed.
*/
void checkControllerInput() {
if (Serial.available() > 0) {
char ch = Serial.read();
#ifdef EN_ECHO
Serial.print(ch);
#endif
if (ch == '\n') { // We have a CR which marks the end of the input.
buffer[bufferPtr++] = '\0'; // Null terminate the string.
processCommand(); // Process the input
bufferPtr = 0; // Reset the buffer Pointer for the next input.
} else if (ch == '\r') {
buffer[bufferPtr++] = '\0'; // Null terminate the string.
processCommand(); // Process the input
bufferPtr = 0; // Reset the buffer Pointer for the next input.
} else if (ch == '\b') { // Just in case we are using a terminal,
bufferPtr--; // process a backspace by removing a character from the input.
if (bufferPtr < 0) {
bufferPtr = 0;
}
#ifdef EN_ECHO
Serial.print(" \b"); // erase the character from the terminal. We already echoed the BS, so replace the character with a space and BS again.
#endif
} else {
// Check for buffer overflow (and avoid it).
if (bufferPtr < sizeof(buffer) - 1) {
buffer[bufferPtr++] = ch; // Not a CR and not a LF, so just accumulate the character.
}
}
}
}

/*
* setup routine.
*
* Initialise everything including:
* - the Serial port for status messages and command input
* - set the various ports for output.
* - display a cutsie "test pattern" on the bar LED.
* - setup Timer2 to generate our interrupts (only for the Interrupt driven version of the program).
* - initialise the RTC (real time clock)
* - output some helpful information.
*/
void setup() {
Serial.begin(115200); // Initialise the Serial port.
//Serial.begin(1200); // Use this Serial.begin if you want to try the experiment described below
// in the interrupts section of setup().
int to = 1000;
while (! Serial && to > 0) { // Wait for the Serial port to be ready, but no longer than 1 second.
delay(1);
}

Serial.println();
Serial.print(F("Dimming Quarantine Clock - version: "));
Serial.println(F(VERSION));
Serial.println(F("Initialising, please wait"));

initClockDisplay();


// Set the digital I/O pins for the remaining days bar LED panel.
for (int i = 0; i < BAR_GRAPH_NUM_LEDS; i++) {
pinMode (barGraphLed[i], OUTPUT);
digitalWrite(barGraphLed[i], LOW);
}
delay(500);

// Output a cute "test pattern" on the bar LED.
for (int i = 0; i < BAR_GRAPH_NUM_LEDS; i++) {
digitalWrite(barGraphLed[i], HIGH); // Turn each LED on one by one and pause after each one.
delay(100);
}
delay(500); // Leave the LED's on for a short time.
for (int i = BAR_GRAPH_NUM_LEDS - 1; i >= 0 ; i--) {
digitalWrite(barGraphLed[i], LOW); // Turn each LED off one by one and pase after each one.
delay(50);
}

Wire.begin(); // Initialise the interface to the RTC.
rtc.begin();

if (! rtc.isrunning()) {
Serial.println(F("**** RTC is not running. Please set the time and date."));
}

int year = EEPROM.read(0);
int month = EEPROM.read(1);
int day = EEPROM.read(2);
int checksum = EEPROM.read(3);

Serial.print("y = "); Serial.print(year);
Serial.print(",m = "); Serial.print(month);
Serial.print(",d = "); Serial.print(day);
Serial.print(",c = "); Serial.println(checksum);

if ((year + month + day) & 0xff != checksum) {
Serial.print(F("*** qdate not set."));
} else {
quarantineEndDate = DateTime(year+2000, month, day, 0, 0, 0);
Serial.print(F("*** qdate: ")); printDate(quarantineEndDate);
//setDaysRemaining(rtc.now()); // Update the days remaining value and display.
}

Serial.println(); Serial.println(F("Use these commands to set the clock"));
showCommands();

Serial.println(F("Ready."));
}


/*
* The main loop.
*
* Give the perdiodic activities a chance to run. These include:
* - Checking if there is any input on Serial.
* - Check whether the time has ticked over and format the new time for display.
* - strobe the clock LED display (only applies for the non-interrupt version of the program).
*/
void loop() {
// put your main code here, to run repeatedly:
unsigned long _now = millis();
checkControllerInput();
checkTime(_now);
checkLightLevels(_now);

// If we are using interrupts to manage the clock display, we don't need this.
#if ! defined( USE_INTERRUPTS)
strobeClockLed(_now);
#endif
}<br>

Brightness.h

#ifndef _BRIGHTNESS_LIB
#define _BRIGHTNESS_LIB

/**
* Control the brightness of the LED's using a (home made) digipot circuit
*
* https://en.wikipedia.org/wiki/Digital_potentiometer
* https://components101.com/articles/how-digital-potentiometer-works-and-how-to-use-it
*
*/

class Brightness {
public:
/**
* Brightness Constructor.
* Store the control pin specifications and
* set the ports as needed.
*
* Note: The digiPotPins must be an array of 4 integers in this version.
*/
Brightness(int ldrPin, int digiPotPins[]);

/**
* The exact (i.e. not maximum, but exact) number of digiPot control pins
* that must be specified to the constructor.
*/
static const int NUM_CONTROL_PINS = 4;

/**
* Reads the LDR and adjusts the brightness according to a
* hard coded table of values.
*/
int checkLightLevels();

/*
* Return the most recently observed ambient light level.
*/
int getLightLevel() {
return _currentLightLevel;
}

/*
* Return the lowest observed ambient light level.
*/
int getMinLightLevel() {
return _minLightLevel;
}

/*
* Return the highest observed ambient light level.
*/
int getMaxLightLevel() {
return _maxLightLevel;
}

/*
* Reset the light min and max metrics so a new range can be measured.
*/
void resetLightMetrics() {
_minLightLevel = 1024;
_maxLightLevel = 0;
}

/*
* Return the current dimming level - i.e. the current digiPot setting.
*/
int getDimmingLevel() {
return _currentDimmingLevel;
}

/**
* Set the brightness (or amount of dimming) level to a number between 0 and NUM_CONTROL_PINS
* inclusive.
*
* Thus if there are 4 control pins, there will be 5 brightness levels (0, 1, 2, 3 and 4).
*/
void setDimmingLevel(int level);

/**
* Print debugging information.
*/
void printDebugInfo();


private:
int _digiPotPins[NUM_CONTROL_PINS];
int _currentDimmingLevel = 0;
int _currentLightLevel = 0;
int _minLightLevel = 1024;
int _maxLightLevel = 0;
int _ldrPin;

};

#endif<br>

Brightness.cpp

#include <Arduino.h>
#include "Brightness.h"


/**
* Brightness Constructor.
* Store the control pin specifications and
* set the ports as needed.
*
* Note: The digiPotPins must be an array of 4 integers in this version.
*/
Brightness::Brightness(int ldrPin, int digiPotPins[]) {
_ldrPin = ldrPin;
for (int i = 0; i < NUM_CONTROL_PINS; i++) {
_digiPotPins[i] = digiPotPins[i];
}

for (int i = 0; i < NUM_CONTROL_PINS; i++) {
pinMode(_digiPotPins[i], OUTPUT);
digitalWrite (_digiPotPins[i], LOW); // Turn the transistor (and thus its resistor off
} // Turn on the first transistor which means no resistors
digitalWrite(_digiPotPins[0], HIGH); // in the variable resistance chain => Max brightness

resetLightMetrics(); // Reset the metrics of min/max brightness observed.
}

/**
* Print debugging information.
*/
void Brightness::printDebugInfo() {
Serial.println(F("Brightness config:"));
Serial.print(F(" LDR Pin: ")); Serial.println(_ldrPin);
Serial.print(F(" digipot Pins: "));
for (int i = 0; i < NUM_CONTROL_PINS; i++) {
if (i) {
Serial.print(F(", "));
}
Serial.print(_digiPotPins[i]);
}
Serial.println();
}


/**
* Set the brightness level to a number between 0 and NUM_CONTROL_PINS
* inclusive.
*
* Thus if there are 4 control pins, there will be 5 brightness levels (0, 1, 2, 3 and 4).
*/
void Brightness::setDimmingLevel(int level) {
if (level < 0 || level > NUM_CONTROL_PINS) {
return;
}
if (level == _currentDimmingLevel) { // Don't do anything if the level didn't change.
return;
}

// Serial.print(F("Settting brightness to: ")); Serial.println(level);
// Serial.print(F(" previously: ")); Serial.println(_currentBrightnessLevel);

digitalWrite(_digiPotPins[_currentDimmingLevel], LOW); // Turn off this level
digitalWrite(_digiPotPins[level], HIGH); // Turn on this level
_currentDimmingLevel = level; // Save this new level for the next check.
}

/**
* Reads the LDR and adjusts the brightness (or amount of dimming) according to a
* according to a hard coded "table" of values.
*/
int Brightness::checkLightLevels() {
_currentLightLevel = analogRead(_ldrPin);
_minLightLevel = min(_minLightLevel, _currentLightLevel);
_maxLightLevel = max(_maxLightLevel, _currentLightLevel);

if (_currentLightLevel > 300) {
setDimmingLevel(0);
} else if (_currentLightLevel > 200) {
setDimmingLevel(1);
} else if (_currentLightLevel > 100) {
setDimmingLevel(2);
} else if (_currentLightLevel > 50) {
setDimmingLevel(3);
} else {
setDimmingLevel(4);
}
}<br>

ClockDisplay.h

#ifndef CLOCKDISPLAY_LIB
#define CLOCKDISPLAY_LIB

/* Use interrupts - program configuration constant
* If defined (the value is unimportant) then the program
* will be configured to use interrupts to control the
* refresh of the clock display.
*
* If not defined, the program will be configured to use
* polling to refresh the clock display. This will have side
* effects when long running processes are active.
*
* Best option: define the USE_INTERRUPTS symbol.
*/
#define USE_INTERRUPTS 1
#define LED_STROBE_INTERVAL 1 /* Interval between clock LED strobe steps = 1 millisecond (or 1000 times per second */

extern volatile int colonDisplayTime;

#ifdef __cplusplus
extern "C" {
#endif

/**
* Function to set the time shown on the clock display.
*
* hour is an integer in the range 0 to 23.
* minute is an integer in the range 0 to 59.
*
* Values supplied outside of these ranges will result in unpredictable behaviour.
*/
void setTime(int hour, int minute);

/**
* Reset the timer handling the blinking of the colon on the display.
* This should be called every time the seconds value changes.
* For example when the seconds value of the RTC clock changes from 1 to 2, or 59 to 0 etc, call this function.
*/
void resetClockColonDisplayTime(int dispTime);

/**
* Set up the clock display DIO pins.
* If we are using Interrupts, this also sets up the
* Timer Interrupt.
*/
void initClockDisplay();


#if ! defined(USE_INTERRUPTS)
/*
* Strobe the Clock LED.
*
* This function is called continuously to determine when it is time to strobe the clock LED display.
* If it is time to strobe the clock display, then the function determines the next time that it must be
* strobed and calls the strobe function.
*
* This routine can have a side effect when long running operations are executed. Long running operations
* prevent this routine from being called in a timely fashion. This can result in the clock display flickering
* or freezing on a single digit.
*
* ** If we are using interrupts to manage the clock display, we don't need this function.
*/
void strobeClockLed(unsigned long _now);
#endif // ! USE_INTERRUPTS


#ifdef __cplusplus
} // extern "C"
#endif // cplusplus

#endif //CLOCKDISPLAY_LIB<br>

ClockDisplay.c (Note the extension is .c, not .cpp)

#include <Arduino.h>
#include "ClockDisplay.h"


#if ! defined( USE_INTERRUPTS)
unsigned long strobeClockTO = 0; // Variable used to track the next time we need to strobe the LED display.
// The strobeClockTO is only relevant for the non-Interrupt driven version of the program.
#endif

volatile int colonDisplayTime = 0; // How long the clock's colon has been on.
// When the second clicks over, this is set to CLOCK_COLON_DISPLAY_TIME
// and the clock display routine counts it down to zero.
// if the value is non zero, the colon is tuned on. When zero
// the colon is turned off.


// Variables for the LED strobe

/* The ledFont array defines the "font" for the LED's.
*
* Each byte corresponds to the "image" of a "character" that is displayed on a 7 segment LED digit.
* On the Arduino Mega, the font value is simply output to Port A (digital pins 22-29 inclusive).
*
* font
* font value
* value binary
* Character hex X G F E D C B A
* 0 3f 0 0 1 1 1 1 1 1
* 1 06 0 0 0 0 1 0 1 0
* 2 5b 0 1 0 1 1 0 1 1
* 3 4f 0 1 0 0 1 1 1 1
* 4 66 0 1 1 0 1 0 1 0
* 5 6d 0 1 1 0 1 1 0 1
* 6 75 0 1 1 1 0 1 0 1
* 7 07 0 0 0 0 0 1 1 1
* 8 7f 0 1 1 1 1 1 1 1
* 9 67 0 1 1 0 0 1 1 1
* - 40 0 1 0 0 0 0 0 0
* space 00 0 0 0 0 0 0 0 0
*
* The value 1 turns an LED segment on, a 0 turns it off. The X bit is unused for the font.
* The segments map to the Arduino mega's digital outputs as follows:
*
* Clock Mega 2560 Arduino
* Display Port A Digital
* Segment bit I/O Port
* A 0 22
* B 1 23
* C 2 24
* D 3 25
* E 4 26
* F 5 27
* G 6 28
* DP n/c n/c
* L1,L2 7 29
*
*/
int ledFont[] = {
0x3f, 0x06, 0x5b, 0x4f, // 0, 1, 2, 3
0x66, 0x6d, 0x7d, 0x07, // 4, 5, 6, 7
0x7F, 0x67, 0x40, 0x00 // 8, 9, -, space
};

volatile int clockDisplay[4] = {0, 0, 0, 0}; // The display image to be displayed on each of the clock LEDs.
int currLed = 0; // The LED currently being shown:
// 0 = tens of hours, 1 = hours, 2 = tens of minutes, 3 = minutes.
// This is used by the clock display routine as an index into the clockDisplay array.


const int NUM_LEDS = sizeof (clockDisplay) / sizeof (clockDisplay[0]); // the number of clock display digits as defined by the size of clockDisplay.

const int clockLedPin[] = { 3, 4, 5, 6 }; // The DIO pins to which the common cathode of the 7 segment LED's is attached.
// [0] is the tens of hours.
// [1] is the units of hours.
// [2] is the tens of minutes.
// [3] is the units of minutes.
/* IMPORTANT NOTE: In V2 (as compared to V1), the common cathode of the 7 segment
* LEDs is NO LONGER connected to this DIO pin. Instead, to support the brightness function,
* each LED's cathode is connected to the brightness digiPot.
* To support the turning on and off of the individual LEDs, a transistor is used.
* The Transistor is a NPN transistor (I used BC337). The base of the transistor is connected to
* the pins defined in clockLEDPin.
*
* Because an NPN transistor is used, the logic on the pin is inverted when compared to V1.
* In V1, the DIO pin was set low to select an individual 7 segment LED.
* In this version (V2), an individual 7 segment LED is selected when the DIO pin is set to HIGH.
*/


/*
* Strobe the clock LED.
*
* Strobing involves turning off the current clock digit (as defined by currLed).
* This is achieved by setting the associated pin (from clockLedPin) to HIGH.
*
* Then apply the next digit's image from the fonts (clockDisplay) and turn on that LED so
* its image can be displayed.
*
* This function also manages the blinking of the clock panels colon LED.
*/
void _strobeClockLed() {
digitalWrite(clockLedPin[currLed], LOW); // Turn the current digit off (see IMPORTANT NOTE above)

currLed = (currLed + 1) % NUM_LEDS; // identify the next digit (wrap around to 0 when NUM_LEDs is reached)
int ledImage = clockDisplay[currLed]; // Get the image of the next digit from the fonts array.
if (colonDisplayTime != 0) { // Work out whether the colon should be on or off.
ledImage |= 0x80; // It should be on, so set bit 7 of the display image to 1
colonDisplayTime--; // count this display time.
}
PORTA = ledImage; // Output the entire image to PORTA (DIO pins 22-29 on the Arduino Mega)
// Serial.print("Clock LED "); Serial.print(currLed); Serial.print(": Writing: "); Serial.print(clockDisplay[currLed], HEX);
// Serial.print(", pin: "); Serial.println(clockLedPin[currLed]);
digitalWrite(clockLedPin[currLed], HIGH); // Finally turn on the digit that should display the image.
}


// Timer 2 compare match Interrupt Service Routine
// -----------------------------------------------
// This routine controls the strobing of the LED display.
// What does strobing mean? basically we turn just one digit of the LED's on at any one
// time (the other three LED's are off).
// Why would we do this?
// Because the LED digits all share the control signals (wires) that determine which
// LED segment(s) must be turned on to display the information we need.
// We only have 8 control signals, but we have 32 (4 digits x 8 LED's per digit). So, we strobe,
// or turn on just one of the LED's one at a time, and simultaneously output the correct "image"
// data to the 8 LED segment control lines.
// This routine is called very rapidly - see the description in setup () for how rapidly. This
// gives the illusion of a clear steady display.
#if defined(USE_INTERRUPTS)
SIGNAL(TIMER2_COMPA_vect) {
_strobeClockLed();
}
#endif

/*
* Strobe the Clock LED.
*
* This function is called continuously to determine when it is time to strobe the clock LED display.
* If it is time to strobe the clock display, then the function determines the next time that it must be
* strobed and calls the strobe function.
*
* This routine can have a side effect when long running operations are executed. Long running operations
* prevent this routine from being called in a timely fashion. This can result in the clock display flickering
* or freezing on a single digit.
*
* ** If we are using interrupts to manage the clock display, we don't need this function.
*/
#if ! defined( USE_INTERRUPTS)
void strobeClockLed(unsigned long _now) {
if (_now >= strobeClockTO) { // Is it time to strobe the clock display?
strobeClockTO = _now + LED_STROBE_INTERVAL; // Yep, calculate the next time to do so and
_strobeClockLed(); // strobe the display.
}
}
#endif


/**
* Function to set the time shown on the clock display.
*
* hour is an integer in the range 0 to 23.
* minute is an integer in the range 0 to 59.
*
* Values supplied outside of these ranges will result in unpredictable behaviour.
*/
void setTime(int hour, int minute) {
// Work out what characters (digits) to put into the clock display.
if (hour < 10) { // is the hour a signle digit?
clockDisplay[0] = ledFont[11]; // Yes, output a blank (as opposed to a leading zero.
} else {
clockDisplay[0] = ledFont[hour / 10]; // No, work out what digit image to display for the 10's of the hour.
}
clockDisplay[1] = ledFont[hour % 10]; // The units digit for the hours.
clockDisplay[2] = ledFont[minute / 10]; // The 10's digit for the minutes (this will display a leading zero if needed).
clockDisplay[3] = ledFont[minute % 10]; // The units digit for the minutes.
}

/**
* Reset the timer handling the blinking of the colon on the display.
* This should be called every time the seconds value changes.
* For example when the seconds value of the RTC clock changes from 1 to 2, or 59 to 0 etc, call this function.
*/
void resetClockColonDisplayTime(int dispTime) {
colonDisplayTime = dispTime; // Yep, reset the colon blink time
}

/**
* Set up the clock display DIO pins.
* If we are using Interrupts, this also sets up the
* Timer Interrupt.
*/
void initClockDisplay() {
DDRA = 0xff; // Set Port A to be output. Port A is used to output the image to display on one of the clock digits.
PORTA = 0x00; // turn the clock LED off.

// Set the digital I/O pins for the clock display's common cathodes.
for (int i = 0; i < NUM_LEDS; i++) {
pinMode(clockLedPin[i], OUTPUT);
digitalWrite(clockLedPin[i], HIGH);
}

#if defined (USE_INTERRUPTS)
// Setup an interrupt Service Routine to manage the display
// of the digits on the clock (which must be stobed).
// This is handled by our Interrupt Service Routine (ISR).
//
// To begin, disable interrupts temporarily. Why bother with this?
// If we didn't disable interrupts, the registers that control interrupts will
// be active. This means that we could risk an interrupt being fired while we are
// part way through setting up the registers. Should that happen, the results will
// be unpredictable, random and in the worst case disastrous. Granted this is low
// probability, but if you are unlucky, you will regret not having done this!
// So, we turn of interrupts, configure what we need and finally, renable interrupts.
noInterrupts();

// We will setup up Timer2 (which is also used by the tone function) to trigger an
// interrupt 1,000 times per second.
// Out interrupt Service Routine (ISR) will stobe the Clock digits to give us a nice
// crisp display.
//
// Why would we bother with this complexity?
//
// The easiest way to explain why is via an experiment:
// 1) Set the Serial monitor to a low speed (e.g. 1200 baud).
// Remember to change this in the Serial.begin as well.
// 2) use the non-interrupt version of this program.
// Comment out the following line near the top of the program
// #define USE_INTERRUPTS 1
// This may be enough, but if not:
// 3) Cause a large volume of data to be displayed on the serial monitor (the easiest way
// is to use the "status" command or enter an invalid command such as an "X" or to
// display the help information).
//
// What you should see is that the clock display will pause from time to time with just one LED lit,
// or it may or have similar undesirable characteristics.
//
// Finally:
// 4) repeat the test using the "interrupt enabled" version of the program (uncomment the #define
// from step 2.
// 5) Reset the Serial.begin and serial monitor to a more sensible baud rate (e.g. 115200).
//
// So, what is happening is that the strobing of the LED's needs to be performed at a consistant rate to maintain
// the crispness of the display. If the "refresh rate" slows down too much (or stops altogether), then the clock
/// display may start to flicker or even freeze periodically.
// This will happen as a result of other activity (e.g. Serial.println, delay and long running code).
// To avoid this, the interrupt mechanism will reliably call our LED strobing ISR 1,000 times per second
// no matter what other long running tasks may be running.
//
// After all of the above, you might be thinking:
// - don't set the Serial monitor to be so slow
// - don't output so many messages.
// or
// - some other work around.
// Yes, you could do those things. But, messages are useful. And, workarounds can tend to grow out of proportion
// in the "difficulty department" as you try to deal with other side affects.
// Maybe it is just me, but I feel that I can sometimes see the clock flicker slightly even at higher baud rates.
// Indeed at 115200 baud, when you run the status command or display the help, the display will flicker.
// Finally, and this is probably the best reason, I'm stuck in quarantine, so I haven't got much else
// to do! That is also probably why I am using the single line comment for all of this rather than a
// /* multi-line comment block */ !!!!!! Although that is starting to get tedious, so I might switch
// to multi-line comment blocks for the rest of this documentation! :-)
// And if you don't appreciate the quarantine reason, this use case (i.e. ensuring things happen when they are supposed
// to happen despite other things that are going on) is what interrupts are for.
//
// ------------------------
//
// Details about the timer registers we will be using can be found in the AVR data sheet.
// In the case of the Mega (AVR Mega2560 data sheet dated 02/2014):
// - Chapter 20 describes all aspects of the features of timer 2 (Chapter 18 on Uno)
// - Chapter 20.10 has a detailed description of the timer 2 registers (chapter 18.11 on Uno)
// - Chapter 33 has a summary of all registers (chapter 36 on Uno)
//
// Set the TCCR2 registers to 0. This effectively cancels any other configuration
// that might be left over in the timer 2 registers.
// TCCR2 is the Timer Counter / Control register for timer2.
// TCCR2 is in 2 parts (i.e. A and B).
TCCR2A = 0; // set timer 2 control registers 0 to turn off
TCCR2B = 0; // all functions.
TCNT2 = 0; // initialize the timer 2 counter value to 0

// set compare match register for 1khz increments
// = (16*10^6) / (1000*64) - 1 (must be <256)
// = 16 MHz clock / (1000 hz * 64x prescaler) - 1
// = 16,000,000 / 64,000 - 1
// = 250 - 1
// = 249
// OCR2A is a single byte (and thus must be < 256)
// so a combination of "prescaler" and frequency is used
// to determine how high to count before generating an interrupt
OCR2A = 249; // = Clock speed / (desired frequency * prescaler value).

// Set CS22 bit for 32x prescaler
// - Refer to section 17.10 of the datasheet
// Basically this divides the 16MHz clock by 32 (= 0.5MHz)
// for the purposes of driving Timer2.
// (CS22 = 1, CS21 = 0, CS20 = 0)
TCCR2B |= (1 << CS22);
// The above two settings cause the counter (TCCNT2) to be incremented by
// one every time the scaled clock (16MHz / 64x) ticks.
// this will occur roughly once every 16,000,000 / 64,000 = 1/250th of a second
// or put another way, 250 times per second.
// so once we get to 249 (remember, we started counting from 0) 1 second will have
// passed.

// Turn on CTC (Clear Timer on Compare match) mode
// Basically, when TCNT2 reaches OCR2A (i.e. 249) TCNT2 is reset to zero.
// This is not terribly useful for our purposes, but stay tuned for more.
// Refer to chapter 20.10.1 for register setting specfics
// (WGM2 = 0, WGM1 = 1, WGM0 = 0) and chapter 20.4 for a
// on the various modes of operation.
TCCR2A |= (1 << WGM21);

// Enable timer compare (Timer 2 Output Compare Match A) interrupt.
// This is "the good bit", it is what we have been working towards.
// Basically when the counter (TCNT2) reaches our limit (OCR2A) an interrupt
// will be generated.
// The interrupt is the Timer 2 Output Compare Match A Interrupt.
// This interrupt is interrupt vector 14 on the Arduino Mega AVR2560
// (on the UNO, it is vector 8).
// In our code, the Interrupt Service Routine (ISR) is nominated by the rather
// curious looking function definition as follows:
// SIGNAL(TIMER2_COMPA_vect) {
// // ISR code goes here
// }
TIMSK2 |= (1 << OCIE2A);

// Now that everything is setup, reenable the interrupts.
// From this point on, the ISR will be called every 1KHz (i.e. 1000 times per second).
// Irrespective of anything else that is going on (such as slow Serial.println calls).
interrupts();
#endif
}<br>

Configuring the Brightness

clipart4210307.png

Once you have set up the project, let it run overnight.

Be sure that the Serial Monitor is connected and running so that you can get the status information out of it "tomorrow". If you do not have the Serial Monitor connected and running, then when you try to connect it "tomorrow", the Arduino will likely reset. This will result in the loss of data that we now need to examine, so you will need to start over. Also make sure that the Serial Monitor is monitoring the correct Arduino if you have more than one connected. Switching the Serial monitor over "tomorrow" will also likely result in the Arduino resetting, losing the data we need and you having to start over.

In the serial monitor, enter the command "status" into the little edit box at the top of the window and click the "send" button to the right of the text box. You can do this to double check that the Serial monitor is setup correctly as I was just talking about. This should result in an output similar to the following:

Command: 'status'
Current system status
Firmware Version: 2.01.00.00
Current Date/Time: DateTime: 2022/3/21 16:38:49
Quarantine end Date: DateTime: 2022/3/25 0:0:0
Quarantine days remaining: 4
System uptime: 85401s
LED Clock refresh: Interrupt driven
Light Levels:
min: 0
max: 364
now: 286
Dimming Level: 1
Brightness config:
LDR Pin: 54
digipot Pins: 8, 9, 10, 11

If you do not get any output, be sure to check that the drop down at the bottom of the serial monitor is set to any of "Newline", "Carriage Return" or "both NL & CR" and not "no line ending".

What we are looking for is the Light Levels part of the output. There are three components to this: "min", "max" and "now". These values correspond to the readings observed on the LDR since the system had last started. It has been pretty overcast and gloomy where I am, so my highest reading was 364, but on brighter days it has gone to about 500.

If you wish to get a second set of readings, you can enter the "reset" command and let it run for another 24 hours then issue the "status" command again. You can issue the "status" command as much as you like, it doesn't clear any accumulated information.

The way I used these readings was to arbitrarily decide that if the light level is more than 300, then I will set maximum brightness (dimming level 0). If between 200 and 300, then I will set the first level of dimming, 100 to 200 is the second level, 50 to 100 the third level and less than 50 the fourth and final level of dimming.

The "dimming level" aligns with the transistor that I turn on to achieve that level of dimming. The transistors are counted from 0. Specifically the first transistor (connected to DIO pin 8) is the 0th level of dimming when turned on. This first transistor - or the 0th transistor - does not add any additional resistors to the circuit when turned on. Hence it is the 0th level of dimming.

The second transistor (connected to DIO pin 9) is the 1st level of dimming. When it is turned on (and the 0th is off), then we have the first additional resistor in the circuit. Current can no longer flow through the 0th transistor (because it is turned off). Consequently, the current must flow through the first 50 ohm resistor until it gets to the second transistor. Since the second transistor is turned on, the current can flow through it to GND - hence this second transistor enables the "first level of dimming" because it adds one resistor to the circuit..

The third transistor (DIO pin 10) is the 2nd level of dimming because when turned on (assuming level 0 and level 1 transistors are off) adds the second resistor.

And so on. Reading the above in conjuntion with the "Brightness Control" portion of the schematic diagram (lower left corner of the diagram) might make this a bit easier to follow if it is still not completely clear how it works.

Customising the dimming thresholds

The code that implements the above is in the checkLightLevels function which can be found in Brightness.cpp

At about line 75, you will see the following if statement:

  if (_currentLightLevel > 300) {
setDimmingLevel(0);
} else if (_currentLightLevel > 200) {
setDimmingLevel(1);
} else if (_currentLightLevel > 100) {
setDimmingLevel(2);
} else if (_currentLightLevel > 50) {
setDimmingLevel(3);
} else {
setDimmingLevel(4);
}<br>

As can be seen from the above, if the currentLightLevel is more than 300 then I set the dimming level to 0 (i.e. no dimming).

Otherwise (300 or less), if the current light level is more than 200, then I set the dimming level to 1. This turns off the first transistor (level 0) and turns on the second one (level 1). And so on.

Simply change these numbers (300, 200, 100 & 50) so that it changes the amount of dimming to suit the readings that you have observed from the "status" command and your personal preferences.

A smaller number means that it must be darker before that level of dimming kicks in.