/*
* FILENAME: Grays_Clock.ino
*
* Code by: Gray Mack
* License: MIT License
* https://choosealicense.com/licenses/mit/
* Created: 11/05/2024
* One Line Description: A simple clock display with up/down buttons as every clock should have
* Board: Arduino nano or Seeed Studios Xaio Samd21
* Select Board:
* Other Hardware: 5 buttons, piezo, DS3121 or DS1307 RTC, TM1637 4x7seg display, 8 ws2812b pixels
* Simulation at: https://wokwi.com/projects/413852839087349761
*
* base code by GitHub Copilot AI chat Chat. Prompt:
Can you write arduino code. Utilize millis instead of delay.
Have 5 section for the code with section comments for "Includes" "Pins" "Constants" "Globals" "Code" and organize the code under those sections.
I have a DS3231 RTC module which should be read frequently.
I have a tm1637 seven segment display with data on pin 10 named DisplayData and clock on pin 8 named DisplayClock. Use the TM1637Display.h library
I have a piezo buzzer on pin 9.
I have 8 ws2812 pixels on pin 7. Use the FastLED library to make a larson scanner.
There are 5 buttons: HourUp, HourDown, MinuteUP, MinuteDown, SecondReset on pins 0, 1, 2, 3, 6 respectively.
Use Button2 library with these buttons.
Use INPUT_PULLUP mode with these buttons.
initialize the serial port at 9600 band and print "Setup complete" at the end of setup.
The button HourUp will increase the hour, rolling over after 12 to 1 and toggle between am and pm and change the RTC modules time and print "Hr+" to console.
The button HourDown will decrease the hour, rolling under from 1 back to 12 and toggle between am and pm and change the RTC modules time and print "Hr-" to console.
The button MinuteUp will increase the minute, rolling over after 59 to 0 and change the RTC modules time and print "Min+" to console.
The button MinuteDown will decrease the minute, rolling under after 0 to 59 and change the RTC modules time and print "Min-" to console.
The button SecondReset will set the RTC modules seconds to 0 and print Sec0 to console.
The button routines also immediately update the display.
The day/month year is not important and can be ignored.
The clock should display in 12 hour mode on the tm1637 seven segment display. The hour should not include leading zero but the minute should for example showing " 3:09"
Update the ClockInterval every 1000ms.
Create a function called TimeToColor that returns a CRGB based on the time. When Hours times 60 + Minutes is closer to (9*60) it returns blue, when closer to (22*60) it returns red and the color blends between these wrapping the percentage over.
Create a function UpdateLarsonScanner function has a static variable tracking the lightOn and a static variable tracking the direction of +1 or -1
Update the LarsonScannerInterval every 1/(NUM_LEDS-1) second calling a function called UpdateLarsonScanner.
* Modifications since then:
* Add serial logging.
* Add nano and samd21 support (wokwi simulator doesnt have samd21)
* Switch to a different digit display library
* Fix the algorithm for the red-blue color shift
* Consistent pin naming
* add zipsounds to buttons
* use setTapHandler instead of setClickHandler
* move millis checks out of loop into the functions
* fix compiler warnings on lambda callbacks
*
* Rev History:
* 10/12/2022 initial code creation
*/
// ----[ configuration ]------------------------------------
#define SERIAL_BAUD 9600
// ----[ included libraries ]------------------------------------
#include <Wire.h>
#include <RTClib.h> // https://github.com/adafruit/RTClib v2.1.4
#include <TM1637.h> // https://github.com/AKJ7/TM1637 TM1637 Driver v2.2.1
#include <FastLED.h> // https://github.com/FastLED/FastLED v3.4.0
#include <Button2.h> // https://github.com/LennartHennigs/Button2 v2.0.1
#include <arduino-timer.h> // https://github.com/contrem/arduino-timer v2.3.1
// ----[ pin definitions ]------------------------------------
#ifdef _SAMD21G18A_
#define HOUR_UP_PIN 0 // << pin difference for nano
#define HOUR_DOWN_PIN 1 // << pin difference for nano
#else // NANO
#define HOUR_UP_PIN A0 // << pin difference for nano
#define HOUR_DOWN_PIN A1 // << pin difference for nano
#endif
#define MINUTE_UP_PIN 2
#define MINUTE_DOWN_PIN 3
#define SECOND_RESET_PIN 6
#define PHOTOCELL_PIN 6
#define LED_PIN 7
#define DISPLAY_CLOCK_PIN 8
#define DISPLAY_DATA_PIN 10
#define RTC_DATA_PIN A4
#define RTC_CLOCK_PIN A5
#define PIEZO_ELEMENT_PIN 9
// ----[ constants ]------------------------------------
const int NUM_LEDS = 8;
const long clockInterval = 1000; // Update interval for display and RTC read
const long larsonInterval = 1000 / (NUM_LEDS - 1); // Update interval for Larson scanner
const int larsonScannerBlueTime = 9*60; // 9am
const int larsonScannerRedTime = (9+12)*60; // 9pm
const int larsonScannerTotalTime = 24*60; // <2400hrs
const int HOUR_TONE_FREQ = 1000;
const int MINUTE_TONE_FREQ = 800;
const int SEC_TONE_FREQ = 400;
const int CLOCK_BRIGHTNESS_PERCENT = 50;
const int LARSON_BRIGHTNESS_255 = 64;
// ----[ Predeclarations ]------------------------------------
void setup();
void loop();
void updateDisplay(bool immediate = false);
CRGB TimeToColor(int hour, int minute);
void updateLarsonScanner();
void adjustHour(int delta);
void adjustMinute(int delta);
void resetSeconds();
bool SoundZipTone(void *);
void StartSoundZip(int StartFreq, int StartDirection);
// ----[ global variables ]------------------------------------
RTC_DS3231 rtc;
TM1637 tm1637(DISPLAY_CLOCK_PIN, DISPLAY_DATA_PIN);
CRGB leds[NUM_LEDS];
Button2 btnHourUp(HOUR_UP_PIN);
Button2 btnHourDown(HOUR_DOWN_PIN);
Button2 btnMinuteUp(MINUTE_UP_PIN);
Button2 btnMinuteDown(MINUTE_DOWN_PIN);
Button2 btnSecondReset(SECOND_RESET_PIN);
int StartSoundFrequency;
int CurrentSoundFrequency;
int SoundDirection;
Timer<1, millis> SoundZip; // a timer to bend the note up or down
// ----[ code ]------------------------------------
void setup() {
Serial.begin(SERIAL_BAUD);
Wire.begin();
rtc.begin();
tm1637.init();
tm1637.setBrightnessPercent(CLOCK_BRIGHTNESS_PERCENT);
tm1637.switchColon();
FastLED.addLeds<NEOPIXEL, LED_PIN>(leds, NUM_LEDS);
FastLED.setBrightness(LARSON_BRIGHTNESS_255);
pinMode(PIEZO_ELEMENT_PIN, OUTPUT);
btnHourUp.setTapHandler([](Button2 &btn) { adjustHour(1); (void)btn; });
btnHourDown.setTapHandler([](Button2 &btn) { adjustHour(-1); (void)btn; });
btnMinuteUp.setTapHandler([](Button2 &btn) { adjustMinute(1); (void)btn; });
btnMinuteDown.setTapHandler([](Button2 &btn) { adjustMinute(-1); (void)btn; });
btnSecondReset.setTapHandler([](Button2 &btn) { resetSeconds(); (void)btn; });
btnSecondReset.setButtonStateFunction([](Button2 &btn) { resetSecondsButtonState(); (voi)btn; })
B
tone(PIEZO_ELEMENT_PIN, 70, 80);d
delay(300);
tone(PIEZO_ELEMENT_PIN, 80, 80);
delay(300);
tone(PIEZO_ELEMENT_PIN, 100, 80);
Serial.println("Setup complete");
}
void loop() {
updateDisplay();
updateLarsonScanner();
btnHourUp.loop();
btnHourDown.loop();
btnMinuteUp.loop();
btnMinuteDown.loop();
btnSecondReset.loop();
SoundZip.tick();
}
void updateDisplay(bool immediate) {
static unsigned long previousDisplayMillis = 0;
unsigned long currentMillis = millis();
if(currentMillis - previousDisplayMillis < clockInterval && !immediate) return;
previousDisplayMillis = currentMillis;
DateTime now = rtc.now();
int hour = now.hour() % 12; // hour is (0 to 23)
if (hour == 0) hour = 12;
int minute = now.minute();
int displayTime = hour * 100 + minute;
tm1637.display(displayTime, false, false, (displayTime > 999) ? 0 : 1);
}
// produce a slow fade from 9a-9p-9a as blue-red-blue
CRGB TimeToColor(int hour, int minute) {
int totalMinutes = hour * 60 + minute; // (0 to 23)*60+59 = (0 to 1439)
totalMinutes -= larsonScannerBlueTime; // zero the fade up
if(totalMinutes < 0) totalMinutes += larsonScannerTotalTime; // wrap negative back around
if(totalMinutes > larsonScannerRedTime-larsonScannerBlueTime) totalMinutes = larsonScannerTotalTime - totalMinutes; // invert the fade down
uint8_t binaryPercent255 = constrain(map(totalMinutes, 0,larsonScannerRedTime-larsonScannerBlueTime, 0,255), 0,255);
return blend(CRGB::Blue, CRGB::Red, binaryPercent255);
}
void updateLarsonScanner() {
static unsigned long previousLarsonMillis = 0;
static int lightOn = 0;
static int direction = 1;
unsigned long currentMillis = millis();
if (currentMillis - previousLarsonMillis < larsonInterval) return;
previousLarsonMillis = currentMillis;
DateTime now = rtc.now();
CRGB color = TimeToColor(now.hour(), now.minute());
fill_solid(leds, NUM_LEDS, CRGB::Black);
leds[lightOn] = color;
FastLED.show();
lightOn += direction;
if (lightOn == NUM_LEDS - 1 || lightOn == 0) {
direction = -direction;
}
}
void adjustHour(int delta) {
DateTime now = rtc.now();
int hour = now.hour() + delta;
if (hour > 23) hour = 0;
if (hour < 0) hour = 23;
rtc.adjust(DateTime(now.year(), now.month(), now.day(), hour, now.minute(), now.second()));
Serial.print("Hr"); Serial.println((delta > 0) ? '+' : '-');
updateDisplay(true);
StartSoundZip(HOUR_TONE_FREQ, (delta > 0) ? +25 : -25);
}
void adjustMinute(int delta) {
DateTime now = rtc.now();
int minute = now.minute() + delta;
if (minute > 59) minute = 0;
if (minute < 0) minute = 59;
rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), minute, now.second()));
Serial.print("M"); Serial.println((delta > 0) ? '+' : '-');
StartSoundZip(MINUTE_TONE_FREQ, (delta > 0) ? +20 : -20);
updateDisplay(true);
if(btnMinuteUp.isPressed() && btnMinuteDown.isPressed()) resetSeconds();
}
void resetSeconds() {
DateTime now = rtc.now();
rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), 0));
Serial.println("Sec0");
StartSoundZip(SEC_TONE_FREQ, +10);
updateDisplay(true);
}
byte resetSecondsButtonState() {
int aval = analogRead(PHOTOCELL_PIN);
if(aval>10) AmbientBrightness = map(aval, 0,1023, 255,16);
}
// Zip sounds - instead of beep, make pitch bend zip tones
bool SoundZipTone(void *)
{
CurrentSoundFrequency += SoundDirection; // a positive or negative number
if( CurrentSoundFrequency > StartSoundFrequency+500
|| CurrentSoundFrequency < StartSoundFrequency-500
|| CurrentSoundFrequency > 10000
|| CurrentSoundFrequency < 0)
{
noTone(PIEZO_ELEMENT_PIN);
return false; // stop
}
else{
tone(PIEZO_ELEMENT_PIN, CurrentSoundFrequency);
return true;
}
}
void StartSoundZip(int StartFreq, int StartDirection)
{
SoundZip.cancel();
StartSoundFrequency = StartFreq;
CurrentSoundFrequency = StartFreq;
SoundDirection = StartDirection;
SoundZip.every(2, SoundZipTone);
}
/*
//TEST ws2812b pixels with larson scanner:
// Includes
#include <FastLED.h>
// Pins
#define LED_PIN 7
// Constants
#define NUM_LEDS 8
#define BRIGHTNESS 64
#define LED_TYPE WS2812
#define COLOR_ORDER GRB
const long larsonInterval = 1000 / (NUM_LEDS - 1); // Update interval for Larson scanner
// Globals
CRGB leds[NUM_LEDS];
unsigned long previousLarsonMillis = 0;
// Code
void setup() {
FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
FastLED.setBrightness(BRIGHTNESS);
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousLarsonMillis >= larsonInterval) {
previousLarsonMillis = currentMillis;
updateLarsonScanner();
}
}
void updateLarsonScanner() {
static int lightOn = 0;
static int direction = 1;
fill_solid(leds, NUM_LEDS, CRGB::Black);
leds[lightOn] = CRGB::Red;
FastLED.show();
lightOn += direction;
if (lightOn == NUM_LEDS - 1 || lightOn == 0) {
direction = -direction;
}
}
*/