Hello everyone.
Hope you are having a wonderful week.
I am making this single thread where I will post any and all of my MEGA projects done with Uno pin footprint standard DF-ROBOT LCD Keypad Shield Display.
This time for the weekly project presentation (as long as I can keep up before my other works arrive), I present a proof of work precision clock software:
MEGA 2560 HyperClock 3.0 FC 16 Precision
Interfaced with:
MAX7219 based FC16 8x8x4 LED matrix,
DF-ROBOT (or clone) LCD Keypad Shield,
Arduino Mega (or MEGA 2560 clone) board.
NOTE: The precision algorithms are more of a proof of work. For practical usage purposes and robust capabilities, hardware solutions like RTC and Temp sensors are recommended. Stubs have been provided appropriately so that users can easily integrate them with the minimum needed knowledge of the basic principles and no other hassles.
Includes interesting effects like bouncing balls and Conwayβs Game of Life which triggers periodically in the LED matrix display. Can be toggled on or off via the LCD Keypad Shield interface.
Brightness controls exposed and integrated into LCD Keypad Shield buttons, along with mode and display change.
Areas for improvement:
More robust and better calibrated Debounce logics.
Full firmware implementation with RTC, GPS, or Temp sensors.
More developed and tweaked Fire Animation.
Source Code:
/**
* Core1D Automation Labs Present:
* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
* β β
* ββββ ββββββ ββββββββββ βββββββββββββββ ββββββββββ βββββββ ββββββββββ| βββ
* ββββ βββββββ βββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββ|βββββ
* βββββββββ βββββββ ββββββββββββββ βββββββββββ βββ βββ ββββββ βββββββ
* βββββββββ βββββ βββββββ ββββββ βββββββββββ βββ βββ ββββββ βββ|βββ
* ββββ βββ βββ βββ βββββββββββ βββββββββββββββββββββββββββββββββββββββ| βββ
* ββββ βββ βββ βββ βββββββββββ βββ βββββββββββββββ βββββββ ββββββββββ| βββ
* β β
* β ββββ ββββββββββββ βββββββ ββββββ βββββββ βββββββ ββββββββ βββββββ /βββ βββββββ
* β βββββ βββββββββββββββββββββ ββββββββ ββββββββ βββββββββ βββ|ββββββββββββ βββββββββββ
* β βββββββββββββββββ βββ ββββββββββββ βββββββ βββ βββ ββββββ βββ βββββββββββ
* β βββββββββββββββββ βββ βββββββββββ βββββββ βββ βββ βββ|ββ βββ ββββββββββββ
* β βββ βββ βββββββββββββββββββββββ βββ ββββββββββββββββββββ βββ| ββββββββ ββββββββββββ
* β βββ βββββββββββ βββββββ βββ βββ βββββββββββ βββββββ βββ| βββββββ βββ βββββββ
* β β
* β Arduino MEGA2560 Precision Clock with MAX7219 FC-16 β
* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
*
* High Stability Software Clock with PPM Drift Compensation
* for Arduino MEGA2560 / ATmega2560 clone boards
*
* Version: 3.0 Precision Edition
* Designed by: Sir Ronnie from Core1D Automation Labs
* License: MIT License
* Target: Arduino MEGA2560 (ATmega2560) + MAX7219 FC-16 LED Matrix + LCD Keypad Shield
*
* HARDWARE CONNECTIONS:
* =====================
* LCD Keypad Shield (DFROBOT-standard, parallel):
* βββββββββββββββββββββ
* β LCD Pin β MEGA Pinβ
* βββββββββββΌββββββββββ
* β RS β 8 β
* β EN β 9 β
* β D4 β 4 β
* β D5 β 5 β
* β D6 β 6 β
* β D7 β 7 β
* β Buttons β A0 β (ADC voltage ladder)
* β VCC β 5V β
* β GND β GND β
* βββββββββββββββββββββ
* (Physically stacks on MEGA β no jumper wires required)
*
* MAX7219 FC-16 LED Matrix Module:
* βββββββββββββββββββββ
* β FC-16 Pin β MEGA β
* βββββββββββββΌββββββββ
* β VCC β 5V β
* β GND β GND β
* β DIN β 51 β
* β CS β 53 β
* β CLK β 52 β
* βββββββββββββββββββββ
*
* Optional External Reference:
* - RTC DS3231: I2C on Pins 20 (SDA) & 21 (SCL)
* - GPS (NMEA): Connect TX β MEGA RX1 (Pin 19)
*
* HARDWARE ASCII SCHEMATIC:
* =========================
*
* Arduino MEGA2560 MAX7219 FC-16
* βββββββββββββββββββββββ βββββββββββββββββββ
* β [USB-B] 5V5 βββΌββββββββββββ€β VCC β
* β GND βββΌββββββββββββ€β GND β
* β 51 βββΌββββββββββββ€β DIN β
* β 53 βββΌββββββββββββ€β CS β
* β 52 βββΌββββββββββββ€β CLK β
* βββββββββββββββββββββββ βββββββββββββββββββ
* β²
* β
* LCD Keypad Shield (Parallel)
* βββββββββββββββββββββββββββββββ
* β RS β Pin 8 D6 β Pin 6 β
* β EN β Pin 9 D7 β Pin 7 β
* β D4 β Pin 4 Btns β A0 β
* β D5 β Pin 5 β
* β VCC, GND from MEGA headers β
* βββββββββββββββββββββββββββββββ
*
*
* PRINCIPLES APPLIED:
* ===================
* β’ Microsecond-resolution software clock driven by micros()
* β’ PPM-based crystal drift correction (board-specific calibration)
* β’ Runtime PPM correction stub for future RTC/GPS sync
* β’ HH:MM:SS dual display: LCD + LED matrix
* β’ LCD buttons for menu, calibration, and mode switching
* β’ Optional serial debug output with drift statistics
*
* PERFORMANCE SPECS:
* ==================
* β’ Resolution: 1 Β΅s internal, 1 s displayed
* β’ Drift Correction: < Β±0.1 s/day with proper PPM setting
* β’ Calibration Range: Β±50000 PPM
* β’ Runtime PPM Update: Adjustable (default: every 300 s)
* β’ Compatible with MEGA2560 16 MHz ceramic resonator or quartz crystal
*
* LIBRARIES & CREDITS
* ===================
*
* 1. MD_MAX72XX by MajicDesigns (Marco Colli)
* - Flexible control library for MAX7219/MAX7221-based LED matrix displays
* - https://github.com/MajicDesigns/MD_MAX72XX
*
* 2. SPI by Arduino
* - Core SPI communication library for high-speed device interfacing
* - https://www.arduino.cc/en/reference/SPI
*
* 3. LiquidCrystal by Arduino, Adafruit, and DFROBOT contributors
* - LCD Keypad Shield support for standard Hitachi HD44780-compatible LCDs
* - https://github.com/arduino-libraries/LiquidCrystal
*
* 4. AVR pgmspace (part of avr-libc, Alf-Egil Bogen and Vegard Wollan)
* - PROGMEM utilities for storing constants in flash memory
* - https://www.nongnu.org/avr-libc/
*
* SPECIAL THANKS
* ==============
* β’ DFROBOT for the original LCD Keypad Shield design
* β’ Arduino community contributors for open-source support
* β’ MajicDesigns for their outstanding MAX72XX library
*
* This firmware combines multiple open-source components.
* Licensed under their respective licenses.
*/
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <LiquidCrystal.h>
#include <avr/pgmspace.h>
// Hardware definitions
constexpr uint8_t HARDWARE_TYPE = MD_MAX72XX::FC16_HW;
constexpr uint8_t MAX_DEVICES = 4;
constexpr uint8_t CS_PIN = 53;
constexpr uint8_t MATRIX_WIDTH = 32;
constexpr uint8_t MATRIX_HEIGHT = 8;
// Brightness control
constexpr uint8_t MIN_BRIGHTNESS = 0;
constexpr uint8_t MAX_BRIGHTNESS = 15;
// LCD Shield pins (standard Arduino LCD Keypad Shield)
constexpr uint8_t LCD_RS = 8;
constexpr uint8_t LCD_EN = 9;
constexpr uint8_t LCD_D4 = 4;
constexpr uint8_t LCD_D5 = 5;
constexpr uint8_t LCD_D6 = 6;
constexpr uint8_t LCD_D7 = 7;
constexpr uint8_t LCD_BACKLIGHT = 10;
constexpr uint8_t BUTTON_PIN = A0;
// Button thresholds for analog reading
constexpr uint16_t BTN_RIGHT = 50;
constexpr uint16_t BTN_UP = 200;
constexpr uint16_t BTN_DOWN = 400;
constexpr uint16_t BTN_LEFT = 600;
constexpr uint16_t BTN_SELECT = 800;
constexpr uint16_t BTN_NONE = 900;
// Timing constants
constexpr uint16_t BUTTON_DEBOUNCE_MS = 150;
constexpr uint16_t CLOCK_UPDATE_MS = 200;
constexpr uint16_t ANIMATION_UPDATE_MS = 50;
constexpr uint16_t LCD_REFRESH_MS = 200;
constexpr uint16_t GAME_OF_LIFE_UPDATE_MS = 500;
constexpr uint32_t ANIMATION_SWITCH_MS = 30000;
constexpr uint16_t FIRMWARE_INFO_DISPLAY_MS = 3000;
constexpr uint16_t FIRMWARE_ANIM_SWITCH_MS = 750;
constexpr uint32_t SECONDS_PER_DAY = 86400UL;
constexpr uint16_t SECONDS_PER_HOUR = 3600;
constexpr uint8_t SECONDS_PER_MINUTE = 60;
/*
DRIFT ANALYSIS:
Based on latest long-run wet test:
- Init time: 15:29 (both clocks synchronized)
- Current time: MEGA shows 17:17, Control shows 17:19
- Arduino elapsed: 1 hour 48 minutes, Real elapsed: 1 hour 50 minutes
- Error: Arduino is approximately 2 minutes slow over 1 hour 50 minutes
(discounting small operational timing errors in seconds)
PPM DRIFT COUNTER:
Error in seconds: -120 seconds
Real elapsed time: (1 Γ 3600) + (50 Γ 60) = 6,600 seconds
PPM = (Error Γ· Real_Time) Γ 1,000,000
PPM = (-120 Γ· 6,600) Γ 1,000,000 = -18,182 PPM
- PPM measures the crystalβs frequency error, not software timing
- Negative PPM = crystal oscillates slower than 16 MHz (Arduino runs slow)
- Positive PPM = crystal oscillates faster than 16 MHz (Arduino runs fast)
- Correction compensates for the crystal's inherent frequency offset
- This is NOT related to millis() drift β itβs purely a hardware oscillator characteristic
CORRECTION VALUE:
Device 0βs crystal runs approximately 1.818% slow, requiring a +18,182 PPM correction
to speed up timing calculations and match real time.
This may vary from board to board β test with a long run on your own hardware
and use the above calculation as a reference to counter crystal drift.
NOTE:
PPM values are board-specific. The example here was measured on a MEGA 2560 clone (ATmega2560).
The value shown is unusually large compared to typical genuine Arduino boards
(usually Β±20β100 PPM) and reflects this specific unitβs oscillator accuracy.
Users should measure and adjust this for their own board, or skip it entirely if
an RTC module is present.
*/
// HIGH-PRECISION TIMING CONSTANTS
constexpr int16_t CLOCK_CORRECTION_PPM = -18182; // Negative because crystal runs slow, needs speeding up
constexpr uint32_t PRECISE_SECOND_MICROS = 1000000UL; // Microseconds per second
constexpr uint32_t DRIFT_CHECK_INTERVAL_SECONDS = 3600UL; // Check drift every hour
constexpr uint32_t MAX_DRIFT_CORRECTION_MS = 100; // Maximum single correction in ms
// Animation types
enum class AnimationType : uint8_t {
FIRE = 0,
GAME_OF_LIFE,
BOUNCING_BALLS,
COUNT
};
// Animation cycle config
constexpr uint32_t ANIMATION_CYCLE_INTERVAL_MS = 20000; // 20 sec
uint32_t lastAnimationChange = 0;
// Global objects
MD_MAX72XX mx(HARDWARE_TYPE, CS_PIN, MAX_DEVICES);
LiquidCrystal lcd(LCD_RS, LCD_EN, LCD_D4, LCD_D5, LCD_D6, LCD_D7);
// Display buffers
uint8_t backgroundBuffer[MATRIX_WIDTH][MATRIX_HEIGHT];
uint8_t displayBuffer[MATRIX_WIDTH][MATRIX_HEIGHT];
// State variables
AnimationType currentAnimation = AnimationType::FIRE;
bool animationEnabled = true;
bool backgroundEnabled = true;
uint8_t ledBrightness = 8;
uint8_t lcdBrightness = 128;
uint32_t lastAnimUpdate = 0;
uint32_t lastClockUpdate = 0;
uint32_t lastButtonRead = 0;
// HIGH-PRECISION TIMING VARIABLES
uint32_t preciseClockMicros = 0; // Microsecond-precision clock
uint32_t lastMicrosUpdate = 0; // Last micros() reading
uint32_t clockSeconds = 0; // Integer seconds for display
int32_t accumulatedDriftMicros = 0; // Accumulated timing drift
uint32_t lastDriftCheck = 0; // Last drift correction time
uint32_t totalRunTimeMicros = 0; // Total runtime for accuracy calculation
// Temperature compensation (optional - requires temperature sensor)
bool temperatureCompensationEnabled = false;
float lastTemperature = 25.0; // Default room temperature
// Animation variables
uint8_t fireMap[MATRIX_WIDTH][MATRIX_HEIGHT];
bool gameOfLifeGrid[MATRIX_WIDTH][MATRIX_HEIGHT];
bool gameOfLifeNext[MATRIX_WIDTH][MATRIX_HEIGHT];
struct Ball {
float x, y;
float vx, vy;
uint8_t trail[8][2];
uint8_t trailIndex;
};
Ball balls[3];
// 3x5 font for digits
constexpr uint8_t DIGIT_FONT[11][5] PROGMEM = {
{0b111, 0b101, 0b101, 0b101, 0b111}, // 0
{0b010, 0b110, 0b010, 0b010, 0b111}, // 1
{0b111, 0b001, 0b111, 0b100, 0b111}, // 2
{0b111, 0b001, 0b111, 0b001, 0b111}, // 3
{0b101, 0b101, 0b111, 0b001, 0b001}, // 4
{0b111, 0b100, 0b111, 0b001, 0b111}, // 5
{0b111, 0b100, 0b111, 0b101, 0b111}, // 6
{0b111, 0b001, 0b001, 0b001, 0b001}, // 7
{0b111, 0b101, 0b111, 0b101, 0b111}, // 8
{0b111, 0b101, 0b111, 0b001, 0b111}, // 9
{0b000, 0b010, 0b000, 0b010, 0b000} // : (colon)
};
constexpr uint8_t DIGIT_WIDTH = 3;
constexpr uint8_t DIGIT_HEIGHT = 5;
constexpr uint8_t COLON_WIDTH = 2;
constexpr uint8_t CHAR_SPACING = 1;
// Initial time settings
constexpr uint8_t INITIAL_HOUR = 19;
constexpr uint8_t INITIAL_MINUTE = 37;
constexpr uint8_t INITIAL_SECOND = 0;
// GOL Automata config
constexpr uint8_t GOL_WIDTH = MATRIX_WIDTH;
constexpr uint8_t GOL_HEIGHT = MATRIX_HEIGHT;
bool golCurrent[GOL_HEIGHT][GOL_WIDTH];
bool golNext[GOL_HEIGHT][GOL_WIDTH];
// LCD display optimization variables
char lastTimeStr[9] = " ";
char lastStatusStr[17] = " ";
uint32_t lastLCDRefresh = 0;
// Animation names stored in PROGMEM
const char ANIM_NAME_FIRE[] PROGMEM = "Fire";
const char ANIM_NAME_LIFE[] PROGMEM = "Life";
const char ANIM_NAME_BALLS[] PROGMEM = "Balls";
const char* const ANIMATION_NAMES[] PROGMEM = {
ANIM_NAME_FIRE,
ANIM_NAME_LIFE,
ANIM_NAME_BALLS
};
// Runtime PPM correction variables
int32_t runtimePPM = CLOCK_CORRECTION_PPM; // Start with your calibrated value
const float PPM_ADAPT_RATE = 0.05f; // Lower = slower adaptation, 0.0 = disabled
// External reference stub (RTC/GPS/NTP)
// Return true and set referenceSeconds when a real reference is wired in.
static bool fetchReferenceSeconds(uint32_t &referenceSeconds) {
// Example (future): referenceSeconds = rtc.getSecondsToday(); return true;
// Example (future): referenceSeconds = gps.getSecondsToday(); return true;
(void)referenceSeconds; // silence unused parameter warning for now
return false; // no reference available yet
}
void setup() {
Serial.begin(9600);
// Uncomment this to check the timer calculations in the serial monitor
// calculateDriftForMeasurement();
// Initialize matrix
mx.begin();
mx.control(MD_MAX72XX::INTENSITY, ledBrightness);
mx.clear();
// Initialize LCD
lcd.begin(16, 2);
pinMode(LCD_BACKLIGHT, OUTPUT);
analogWrite(LCD_BACKLIGHT, lcdBrightness);
// Initialize high-precision clock
initializePreciseClock();
// Clear buffers
memset(backgroundBuffer, 0, sizeof(backgroundBuffer));
memset(displayBuffer, 0, sizeof(displayBuffer) );
// Initialize animations
initializeAnimations();
// Display startup message
lcd.print(F("Precision Clock"));
lcd.setCursor(0, 1);
lcd.print(F("Initializing..."));
delay(2000);
initializeLCDDisplay();
currentAnimation = AnimationType::FIRE;
initGameOfLife(); // prepares Game of Life grid
// Print timing information to serial
Serial.println(F("High-Precision Clock Initialized"));
Serial.print(F("Clock correction: "));
Serial.print(CLOCK_CORRECTION_PPM);
Serial.println(F(" PPM"));
}
void loop() {
// HIGH-PRECISION TIMING UPDATE - This runs first and most frequently
updatePreciseClock();
const uint32_t currentTime = millis();
// Handle serial commands for calibration and debugging
handleSerialCommands();
// Cycle Clock LED Animations
cycleAnimation();
// Run active animation
updateAnimation();
// Handle millis() overflow
static uint32_t lastOverflowCheck = 0;
if (currentTime < lastOverflowCheck) {
handleMillisOverflow(currentTime);
}
lastOverflowCheck = currentTime;
// Read buttons with debouncing
if (currentTime - lastButtonRead > BUTTON_DEBOUNCE_MS) {
handleButtons();
lastButtonRead = currentTime;
}
// Update LCD display
if (currentTime - lastClockUpdate > CLOCK_UPDATE_MS) {
updateLCDDisplay();
lastClockUpdate = currentTime;
}
// Render display (animations + clock)
if (currentTime - lastAnimUpdate > ANIMATION_UPDATE_MS) {
renderDisplay();
lastAnimUpdate = currentTime;
}
}
void initGameOfLife() {
for (uint8_t y = 0; y < GOL_HEIGHT; y++) {
for (uint8_t x = 0; x < GOL_WIDTH; x++) {
golCurrent[y][x] = random(0, 2);
}
}
}
uint8_t golCountNeighbors(uint8_t x, uint8_t y) {
uint8_t count = 0;
for (int8_t dy = -1; dy <= 1; dy++) {
for (int8_t dx = -1; dx <= 1; dx++) {
if (dx == 0 && dy == 0) continue;
int nx = (x + dx + GOL_WIDTH) % GOL_WIDTH;
int ny = (y + dy + GOL_HEIGHT) % GOL_HEIGHT;
if (golCurrent[ny][nx]) count++;
}
}
return count;
}
void cycleAnimation() {
uint32_t now = millis();
if (now - lastAnimationChange >= ANIMATION_CYCLE_INTERVAL_MS) {
lastAnimationChange = now;
// Increment and wrap using COUNT
uint8_t nextAnim = (static_cast<uint8_t>(currentAnimation) + 1) % static_cast<uint8_t>(AnimationType::COUNT);
currentAnimation = static_cast<AnimationType>(nextAnim);
}
}
void initializePreciseClock() {
const uint32_t currentMicros = micros();
// Calculate initial time in seconds only (don't use microseconds for absolute time)
const uint32_t initialTimeSeconds = (static_cast<uint32_t>(INITIAL_HOUR) * SECONDS_PER_HOUR) +
(static_cast<uint32_t>(INITIAL_MINUTE) * SECONDS_PER_MINUTE) +
static_cast<uint32_t>(INITIAL_SECOND);
// Initialize timing variables
lastMicrosUpdate = currentMicros;
preciseClockMicros = 0; // Start counting microseconds from initialization
clockSeconds = initialTimeSeconds; // Set the clock to the desired initial time
accumulatedDriftMicros = 0;
lastDriftCheck = 0; // Reset to start drift checking from initialization
totalRunTimeMicros = 0;
Serial.println(F("=== CLOCK INITIALIZATION DEBUG ==="));
Serial.print(F("Initial time set to: "));
Serial.print(getHour()); Serial.print(F(":"));
if (getMinute() < 10) Serial.print(F("0"));
Serial.print(getMinute()); Serial.print(F(":"));
if (getSecond() < 10) Serial.print(F("0"));
Serial.println(getSecond());
Serial.print(F("clockSeconds initialized to: ")); Serial.println(clockSeconds);
Serial.println(F("Microsecond precision tracking started"));
Serial.println(F("==================================="));
}
void updatePreciseClock() {
const uint32_t currentMicros = micros();
uint32_t deltaMicros;
// Handle micros() overflow (every ~70 minutes)
if (currentMicros < lastMicrosUpdate) {
deltaMicros = (0xFFFFFFFFUL - lastMicrosUpdate) + currentMicros + 1;
} else {
deltaMicros = currentMicros - lastMicrosUpdate;
}
// Apply PPM correction (runtimePPM instead of fixed constant)
if (runtimePPM != 0) {
int64_t correctionMicros = ((int64_t)deltaMicros * -(int32_t)runtimePPM) / 1000000LL;
int64_t correctedDelta = (int64_t)deltaMicros + correctionMicros;
if (correctedDelta < 1) {
correctedDelta = deltaMicros / 2; // Fallback safety
}
deltaMicros = (uint32_t)correctedDelta;
}
// Apply temperature compensation if enabled
if (temperatureCompensationEnabled) {
deltaMicros = applyTemperatureCompensation(deltaMicros);
}
// Update precise clock - accumulate microseconds
preciseClockMicros += deltaMicros;
totalRunTimeMicros += deltaMicros;
lastMicrosUpdate = currentMicros;
// Carry-over for second calculation
static uint32_t microsecondsCarryOver = 0;
microsecondsCarryOver += deltaMicros;
// If we crossed a second boundary
if (microsecondsCarryOver >= PRECISE_SECOND_MICROS) {
uint32_t secondsToAdd = microsecondsCarryOver / PRECISE_SECOND_MICROS;
microsecondsCarryOver %= PRECISE_SECOND_MICROS;
clockSeconds = (clockSeconds + secondsToAdd) % SECONDS_PER_DAY;
static uint8_t debugCount = 0;
if (debugCount < 10) {
Serial.print(F("Clock update #")); Serial.print(debugCount);
Serial.print(F(": added ")); Serial.print(secondsToAdd); Serial.print(F(" second(s)"));
Serial.print(F(", clockSeconds=")); Serial.print(clockSeconds);
Serial.print(F(", deltaMicros=")); Serial.print(deltaMicros);
Serial.print(F(", Runtime PPM=")); Serial.print(runtimePPM);
Serial.println();
debugCount++;
}
}
// Periodic drift correction check
if (totalRunTimeMicros - lastDriftCheck > (DRIFT_CHECK_INTERVAL_SECONDS * PRECISE_SECOND_MICROS)) {
performDriftCorrection();
lastDriftCheck = totalRunTimeMicros;
}
}
uint32_t applyTemperatureCompensation(uint32_t deltaMicros) {
// Simple linear model: about -0.04 ppm/Β°C away from 25Β°C (example only).
const float temperature = 25.0f; // TODO: read from a real sensor
const float tempPpm = (temperature - 25.0f) * -0.04f;
// Use floating math so sub-ppm effects aren't truncated to zero
const double correction = ((double)deltaMicros * (double)tempPpm) / 1000000.0;
int32_t corrected = (int32_t)((double)deltaMicros + correction);
if (corrected < 1) corrected = 1; // safety
return (uint32_t)corrected;
}
// -----------------------------------------------------------------------------
// performDriftCorrection()
// -----------------------------------------------------------------------------
// Called periodically from updatePreciseClock().
// - If an external reference is available (RTC/GPS/NTP), compute the error,
// adapt runtimePPM smoothly toward the measured PPM, and snap the time.
// - If no reference is available, do nothing (no surprises).
// -----------------------------------------------------------------------------
void performDriftCorrection() {
uint32_t referenceSeconds = 0;
if (!fetchReferenceSeconds(referenceSeconds)) {
// No external reference β keep running on runtimePPM only.
#ifdef SERIAL_DEBUG
Serial.println(F("[DriftCorrection] Skipped β no external reference available."));
#endif
return;
}
// How much time actually passed since the last check
uint32_t elapsedSeconds = (totalRunTimeMicros - lastDriftCheck) / PRECISE_SECOND_MICROS;
if (elapsedSeconds == 0) elapsedSeconds = 1; // safety
// Signed error: reference - our clock
int32_t errorSeconds = (int32_t)referenceSeconds - (int32_t)clockSeconds;
if (errorSeconds == 0) {
#ifdef SERIAL_DEBUG
Serial.println(F("[DriftCorrection] No error detected."));
#endif
return;
}
// Measured PPM over the actual elapsed time
const double measuredPPM = ((double)errorSeconds * 1000000.0) / (double)elapsedSeconds;
// Smoothly adapt runtimePPM (uses your existing PPM_ADAPT_RATE)
runtimePPM = (int32_t)((1.0f - PPM_ADAPT_RATE) * (float)runtimePPM
+ PPM_ADAPT_RATE * (float)measuredPPM);
// Apply a direct snap to remove accumulated error now (wrap to 24h)
int32_t corrected = (int32_t)clockSeconds + errorSeconds;
corrected %= (int32_t)SECONDS_PER_DAY;
if (corrected < 0) corrected += SECONDS_PER_DAY;
clockSeconds = (uint32_t)corrected;
// Optional debug
Serial.print(F("[DriftCorrection] err=")); Serial.print(errorSeconds);
Serial.print(F("s, elapsed=")); Serial.print(elapsedSeconds);
Serial.print(F("s, measuredPPM=")); Serial.print(measuredPPM, 2);
Serial.print(F(", runtimePPM=")); Serial.println(runtimePPM);
}
void handleMillisOverflow(const uint32_t currentTime) {
lastAnimUpdate = 0;
lastClockUpdate = 0;
lastButtonRead = 0;
lastLCDRefresh = 0;
// Don't reset precise timing variables - they handle their own overflow
Serial.println(F("millis() overflow handled"));
}
uint8_t getHour() {
return (clockSeconds / SECONDS_PER_HOUR) % 24;
}
uint8_t getMinute() {
return (clockSeconds % SECONDS_PER_HOUR) / SECONDS_PER_MINUTE;
}
uint8_t getSecond() {
return clockSeconds % SECONDS_PER_MINUTE;
}
// Get fractional seconds for ultra-precise display (0-999 milliseconds)
uint16_t getMillisecond() {
return (preciseClockMicros % PRECISE_SECOND_MICROS) / 1000;
}
// Get timing accuracy information
float getClockAccuracyPPM() {
if (totalRunTimeMicros < PRECISE_SECOND_MICROS) return 0.0; // Not enough runtime
// Prevent division by zero
if (totalRunTimeMicros == 0) return NAN;
// Compare internal precise time vs millis() based time
uint32_t millisBasedMicros = millis() * 1000UL;
int32_t driftMicros = (int32_t)preciseClockMicros - (int32_t)millisBasedMicros;
// Calculate PPM: (drift / total_time) * 1,000,000
return ((float)driftMicros * 1000000.0) / (float)totalRunTimeMicros;
}
// HELPER: Manual drift calculation for your specific case
void calculateDriftForMeasurement() {
Serial.println(F("\n=== YOUR MEASUREMENT ANALYSIS ==="));
Serial.println(F("Based on your data:"));
Serial.println(F("Real time: 13:16 β 13:33 (17 minutes)"));
Serial.println(F("Arduino: 13:16 β 13:31 (15 minutes)"));
Serial.println(F("Drift: Arduino is 2 minutes slow"));
Serial.println();
// Calculate PPM
float realElapsedMinutes = 17.0;
float arduinoElapsedMinutes = 15.0;
float errorMinutes = arduinoElapsedMinutes - realElapsedMinutes; // -2 minutes
float errorSeconds = errorMinutes * 60.0; // -120 seconds
float realElapsedSeconds = realElapsedMinutes * 60.0; // 1020 seconds
float calculatedPPM = (errorSeconds / realElapsedSeconds) * 1000000.0;
Serial.print(F("Calculated PPM: "));
Serial.print(calculatedPPM, 1);
Serial.println(F(" (negative = Arduino runs slow)"));
Serial.println();
Serial.print(F("Recommended CLOCK_CORRECTION_PPM: "));
Serial.print((int16_t)calculatedPPM);
Serial.println();
Serial.println(F("This will speed up your Arduino's clock"));
Serial.println(F("===================================\n"));
}
void initializeAnimations() {
memset(fireMap, 0, sizeof(fireMap));
randomSeed(analogRead(A1));
for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
for (uint8_t y = 0; y < MATRIX_HEIGHT; y++) {
gameOfLifeGrid[x][y] = random(100) < 30;
}
}
for (uint8_t i = 0; i < 3; i++) {
balls[i].x = random(MATRIX_WIDTH);
balls[i].y = random(MATRIX_HEIGHT);
balls[i].vx = (random(200) - 100) / 100.0f;
balls[i].vy = (random(200) - 100) / 100.0f;
balls[i].trailIndex = 0;
for (uint8_t j = 0; j < 8; j++) {
balls[i].trail[j][0] = static_cast<uint8_t>(balls[i].x);
balls[i].trail[j][1] = static_cast<uint8_t>(balls[i].y);
}
}
}
void updateAnimation() {
switch (currentAnimation) {
case AnimationType::FIRE:
updateFireEffect();
break;
case AnimationType::GAME_OF_LIFE:
updateGameOfLife();
break;
case AnimationType::BOUNCING_BALLS:
updateBouncingBalls();
break;
}
}
void updateFireEffect() {
for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
fireMap[x][MATRIX_HEIGHT - 1] = random(100, 255);
}
for (uint8_t y = 0; y < MATRIX_HEIGHT - 1; y++) {
for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
const uint8_t cooling = random(0, 2);
const uint8_t left = (x - 1 + MATRIX_WIDTH) % MATRIX_WIDTH;
const uint8_t right = (x + 1) % MATRIX_WIDTH;
const int newHeat = (fireMap[left][y + 1] + fireMap[x][y + 1] + fireMap[right][y + 1]) / 3 - cooling;
fireMap[x][y] = max(0, newHeat);
}
}
for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
for (uint8_t y = 0; y < MATRIX_HEIGHT; y++) {
backgroundBuffer[x][y] = (fireMap[x][y] > 60) ? 1 : 0;
}
}
}
void updateGameOfLife() {
static uint32_t lastUpdate = 0;
if (millis() - lastUpdate < GAME_OF_LIFE_UPDATE_MS) return;
lastUpdate = millis();
for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
for (uint8_t y = 0; y < MATRIX_HEIGHT; y++) {
const uint8_t neighbors = countNeighbors(x, y);
if (gameOfLifeGrid[x][y]) {
gameOfLifeNext[x][y] = (neighbors == 2 || neighbors == 3);
} else {
gameOfLifeNext[x][y] = (neighbors == 3);
}
}
}
memcpy(gameOfLifeGrid, gameOfLifeNext, sizeof(gameOfLifeGrid));
for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
for (uint8_t y = 0; y < MATRIX_HEIGHT; y++) {
backgroundBuffer[x][y] = gameOfLifeGrid[x][y] ? 1 : 0;
}
}
if (random(100) < 2) {
const uint8_t rx = random(MATRIX_WIDTH);
const uint8_t ry = random(MATRIX_HEIGHT);
gameOfLifeGrid[rx][ry] = !gameOfLifeGrid[rx][ry];
}
}
uint8_t countNeighbors(const uint8_t x, const uint8_t y) {
uint8_t count = 0;
for (int8_t dx = -1; dx <= 1; dx++) {
for (int8_t dy = -1; dy <= 1; dy++) {
if (dx == 0 && dy == 0) continue;
const uint8_t nx = (x + dx + MATRIX_WIDTH) % MATRIX_WIDTH;
const uint8_t ny = (y + dy + MATRIX_HEIGHT) % MATRIX_HEIGHT;
if (gameOfLifeGrid[nx][ny]) count++;
}
}
return count;
}
void updateBouncingBalls() {
static uint32_t lastBallUpdate = 0;
uint32_t now = millis();
// Adjust 50ms to taste (~20 FPS). Increase for slower, decrease for faster
if (now - lastBallUpdate < 50) return;
lastBallUpdate = now;
memset(backgroundBuffer, 0, sizeof(backgroundBuffer));
for (uint8_t i = 0; i < 3; i++) {
balls[i].trail[balls[i].trailIndex][0] = static_cast<uint8_t>(balls[i].x);
balls[i].trail[balls[i].trailIndex][1] = static_cast<uint8_t>(balls[i].y);
balls[i].trailIndex = (balls[i].trailIndex + 1) % 8;
balls[i].x += balls[i].vx;
balls[i].y += balls[i].vy;
if (balls[i].x <= 0 || balls[i].x >= MATRIX_WIDTH - 1) {
balls[i].vx = -balls[i].vx;
balls[i].x = constrain(balls[i].x, 0, MATRIX_WIDTH - 1);
}
if (balls[i].y <= 0 || balls[i].y >= MATRIX_HEIGHT - 1) {
balls[i].vy = -balls[i].vy;
balls[i].y = constrain(balls[i].y, 0, MATRIX_HEIGHT - 1);
}
for (uint8_t j = 0; j < 8; j++) {
if (j % 2 == 0) {
const uint8_t tx = balls[i].trail[j][0];
const uint8_t ty = balls[i].trail[j][1];
if (tx < MATRIX_WIDTH && ty < MATRIX_HEIGHT) {
backgroundBuffer[tx][ty] = 1;
}
}
}
const uint8_t bx = static_cast<uint8_t>(balls[i].x);
const uint8_t by = static_cast<uint8_t>(balls[i].y);
if (bx < MATRIX_WIDTH && by < MATRIX_HEIGHT) {
backgroundBuffer[bx][by] = 1;
}
}
}
void renderDisplay() {
if (backgroundEnabled && animationEnabled) {
updateAnimation();
} else {
memset(backgroundBuffer, 0, sizeof(backgroundBuffer));
}
renderClock();
combineBuffers();
}
void renderClock() {
memset(displayBuffer, 0, sizeof(displayBuffer));
const uint8_t h = getHour();
const uint8_t m = getMinute();
const uint8_t s = getSecond();
const char timeChars[8] = {
static_cast<char>('0' + (h / 10)),
static_cast<char>('0' + (h % 10)),
':',
static_cast<char>('0' + (m / 10)),
static_cast<char>('0' + (m % 10)),
':',
static_cast<char>('0' + (s / 10)),
static_cast<char>('0' + (s % 10))
};
uint8_t totalWidth = 0;
for (uint8_t i = 0; i < 8; i++) {
if (timeChars[i] == ':') {
totalWidth += COLON_WIDTH;
} else {
totalWidth += DIGIT_WIDTH + (i < 7 ? CHAR_SPACING : 0);
}
}
const uint8_t startX = max(0, (MATRIX_WIDTH - totalWidth) / 2) + 1;
uint8_t currentX = startX;
for (uint8_t i = 0; i < 8; i++) {
if (timeChars[i] == ':') {
if (currentX < MATRIX_WIDTH) {
if (2 < MATRIX_HEIGHT) displayBuffer[currentX][2] = 1;
if (5 < MATRIX_HEIGHT) displayBuffer[currentX][5] = 1;
}
currentX += COLON_WIDTH;
} else {
const uint8_t digit = timeChars[i] - '0';
renderDigit(digit, currentX, 1);
currentX += DIGIT_WIDTH + CHAR_SPACING;
}
if (currentX >= MATRIX_WIDTH - DIGIT_WIDTH) break;
}
}
void renderDigit(const uint8_t digit, const uint8_t startX, const uint8_t startY) {
if (digit > 10) return;
for (uint8_t dy = 0; dy < DIGIT_HEIGHT; dy++) {
const uint8_t fontRow = pgm_read_byte(&DIGIT_FONT[digit][dy]);
for (uint8_t dx = 0; dx < DIGIT_WIDTH; dx++) {
const uint8_t pixelX = startX + dx;
const uint8_t pixelY = startY + dy;
if (pixelX < MATRIX_WIDTH && pixelY < MATRIX_HEIGHT) {
const bool pixel = (fontRow >> (DIGIT_WIDTH - 1 - dx)) & 1;
if (pixel) {
displayBuffer[pixelX][pixelY] = 1;
}
}
}
}
}
void combineBuffers() {
for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
for (uint8_t y = 0; y < MATRIX_HEIGHT; y++) {
const bool bg = backgroundBuffer[x][y];
const bool fg = displayBuffer[x][y];
bool result = false;
if (fg) {
result = !bg;
} else {
result = bg;
}
const uint8_t reversedX = (MATRIX_WIDTH - 1) - x;
mx.setPoint(y, reversedX, result);
}
}
}
void handleButtons() {
const uint16_t buttonValue = analogRead(BUTTON_PIN);
if (buttonValue < BTN_RIGHT) {
backgroundEnabled = !backgroundEnabled;
updateLCDDisplay();
}
else if (buttonValue < BTN_UP) {
animationEnabled = !animationEnabled;
updateLCDDisplay();
}
else if (buttonValue < BTN_DOWN) {
ledBrightness = (ledBrightness + 3) % (MAX_BRIGHTNESS + 1);
if (ledBrightness < MIN_BRIGHTNESS) ledBrightness = MIN_BRIGHTNESS;
mx.control(MD_MAX72XX::INTENSITY, ledBrightness);
updateLCDDisplay();
}
else if (buttonValue < BTN_LEFT) {
lcdBrightness = (lcdBrightness == 255) ? 64 : ((lcdBrightness == 64) ? 128 : 255);
analogWrite(LCD_BACKLIGHT, lcdBrightness);
updateLCDDisplay();
}
else if (buttonValue < BTN_SELECT) {
showFirmwareInfo();
}
else if (buttonValue > BTN_NONE) {
static uint32_t lastAnimSwitch = 0;
if (millis() - lastAnimSwitch > ANIMATION_SWITCH_MS) {
currentAnimation = static_cast<AnimationType>((static_cast<uint8_t>(currentAnimation) + 1) % static_cast<uint8_t>(AnimationType::COUNT));
lastAnimSwitch = millis();
updateLCDDisplay();
}
}
}
// Grabage chars protected LCD Display function
void updateLCDDisplay() {
if (millis() - lastLCDRefresh < LCD_REFRESH_MS) return;
lastLCDRefresh = millis();
// Display time with higher precision option
char currentTimeStr[12]; // Extended for milliseconds
static bool showMilliseconds = false;
static uint32_t lastToggle = 0;
// Toggle between seconds and milliseconds display every 5 seconds
if (millis() - lastToggle > 5000) {
showMilliseconds = !showMilliseconds;
lastToggle = millis();
}
if (showMilliseconds) {
snprintf(currentTimeStr, sizeof(currentTimeStr), "%02d:%02d.%03d",
getHour(), getMinute(), getMillisecond());
} else {
snprintf(currentTimeStr, sizeof(currentTimeStr), "%02d:%02d:%02d",
getHour(), getMinute(), getSecond());
}
if (strcmp(currentTimeStr, lastTimeStr) != 0) {
lcd.setCursor(5, 0); // Adjusted position
lcd.print(currentTimeStr);
strcpy(lastTimeStr, currentTimeStr);
}
// Build enhanced status string with accuracy info
char currentStatusStr[17];
char animName[8];
strcpy_P(animName, (char*)pgm_read_word(&(ANIMATION_NAMES[static_cast<uint8_t>(currentAnimation)])));
// Show accuracy every 10 seconds
static uint32_t lastAccuracyDisplay = 0;
static bool showAccuracy = false;
if (millis() - lastAccuracyDisplay > 10000) {
showAccuracy = !showAccuracy;
lastAccuracyDisplay = millis();
}
// Accuracy mode only after 1 minute runtime
if (showAccuracy && totalRunTimeMicros > 60000000UL) {
float accuracy = getClockAccuracyPPM();
if (!isnan(accuracy) && fabs(accuracy) < 1000000.0f) { // Valid sanity check
snprintf(currentStatusStr, sizeof(currentStatusStr), "Acc:%+.1fppm ", accuracy);
} else {
// If accuracy not ready or invalid, show default info instead
snprintf(currentStatusStr, sizeof(currentStatusStr), "%s %s B:%02d ",
animName,
animationEnabled ? (backgroundEnabled ? "ON" : "FG") : "OFF",
ledBrightness);
}
} else {
snprintf(currentStatusStr, sizeof(currentStatusStr), "%s %s B:%02d ",
animName,
animationEnabled ? (backgroundEnabled ? "ON" : "FG") : "OFF",
ledBrightness);
}
// Only update status if it changed
if (strcmp(currentStatusStr, lastStatusStr) != 0) {
lcd.setCursor(0, 1);
lcd.print(currentStatusStr);
strcpy(lastStatusStr, currentStatusStr);
}
}
void initializeLCDDisplay() {
lcd.clear();
lcd.print(F("Time:"));
// Clear last strings to force initial update
strcpy(lastTimeStr, "");
strcpy(lastStatusStr, "");
updateLCDDisplay();
}
void showFirmwareInfo() {
// Store current animation state
const AnimationType originalAnimation = currentAnimation;
const bool originalAnimationEnabled = animationEnabled;
// Show enhanced firmware info
lcd.clear();
lcd.print(F("HyprClck Precision"));
lcd.setCursor(0, 1);
// Display different info screens
uint32_t startTime = millis();
uint8_t infoScreen = 0;
uint32_t lastScreenChange = startTime;
while (millis() - startTime < FIRMWARE_INFO_DISPLAY_MS * 2) { // Extended display time
// Change info screen every second
if (millis() - lastScreenChange > 1000) {
infoScreen = (infoScreen + 1) % 4;
lastScreenChange = millis();
lcd.setCursor(0, 1);
lcd.print(F(" ")); // Clear line
lcd.setCursor(0, 1);
switch (infoScreen) {
case 0:
lcd.print(F("v2.0 - HiPrec"));
break;
case 1:
lcd.print(F("PPM Corr:"));
lcd.print(CLOCK_CORRECTION_PPM);
break;
case 2:
if (totalRunTimeMicros > 60000000UL) {
lcd.print(F("Acc:"));
lcd.print(getClockAccuracyPPM(), 1);
lcd.print(F("ppm"));
} else {
lcd.print(F("Measuring..."));
}
break;
case 3:
lcd.print(F("Runtime:"));
lcd.print(totalRunTimeMicros / 1000000UL);
lcd.print(F("s"));
break;
}
}
// Change animation every 750ms during info display
if ((millis() - startTime) % FIRMWARE_ANIM_SWITCH_MS < 50) {
currentAnimation = static_cast<AnimationType>((static_cast<uint8_t>(currentAnimation) + 1) % static_cast<uint8_t>(AnimationType::COUNT));
}
// Keep display updating
if (originalAnimationEnabled) {
renderDisplay();
}
delay(10);
}
// Restore original animation state
currentAnimation = originalAnimation;
animationEnabled = originalAnimationEnabled;
// Reinitialize LCD display
initializeLCDDisplay();
}
// Calibration function - call this to measure and adjust clock accuracy
void calibrateClockAccuracy() {
Serial.println(F("\n=== ENHANCED CLOCK CALIBRATION ==="));
Serial.println(F("Current calibration data:"));
Serial.print(F("Runtime: "));
Serial.print(totalRunTimeMicros / 1000000UL);
Serial.println(F(" seconds"));
if (totalRunTimeMicros > 60000000UL) { // More than 1 minute
float measuredAccuracy = getClockAccuracyPPM();
Serial.print(F("Measured accuracy: "));
Serial.print(measuredAccuracy, 1);
Serial.println(F(" PPM"));
// Calculate recommended correction
int16_t recommendedCorrection = CLOCK_CORRECTION_PPM - (int16_t)measuredAccuracy;
Serial.print(F("Current correction: "));
Serial.print(CLOCK_CORRECTION_PPM);
Serial.println(F(" PPM"));
Serial.print(F("Recommended new correction: "));
Serial.print(recommendedCorrection);
Serial.println(F(" PPM"));
Serial.println();
Serial.println(F("To apply: Change CLOCK_CORRECTION_PPM to the recommended value"));
} else {
Serial.println(F("Need more runtime for accurate measurement (minimum 1 minute)"));
}
Serial.println(F("MANUAL CALIBRATION GUIDE:"));
Serial.println(F("1. Note real time and Arduino time"));
Serial.println(F("2. Let run for 15+ minutes"));
Serial.println(F("3. Note both times again"));
Serial.println(F("4. Calculate: PPM = (Arduino_error_in_seconds / real_elapsed_seconds) * 1,000,000"));
Serial.println(F("5. If Arduino is SLOW, use NEGATIVE PPM"));
Serial.println(F("6. If Arduino is FAST, use POSITIVE PPM"));
Serial.println(F("=====================================\n"));
}
// Function to set time manually (useful for initial synchronization)
void setTime(uint8_t hour, uint8_t minute, uint8_t second) {
if (hour > 23 || minute > 59 || second > 59) return; // Invalid time
const uint32_t currentMicros = micros();
// Calculate new time values
const uint32_t newTimeMicros = (static_cast<uint32_t>(hour) * SECONDS_PER_HOUR * PRECISE_SECOND_MICROS) +
(static_cast<uint32_t>(minute) * SECONDS_PER_MINUTE * PRECISE_SECOND_MICROS) +
(static_cast<uint32_t>(second) * PRECISE_SECOND_MICROS);
const uint32_t newTimeSeconds = (static_cast<uint32_t>(hour) * SECONDS_PER_HOUR) +
(static_cast<uint32_t>(minute) * SECONDS_PER_MINUTE) +
static_cast<uint32_t>(second);
// Set the time
preciseClockMicros = newTimeMicros;
clockSeconds = newTimeSeconds;
lastMicrosUpdate = currentMicros;
// Reset drift tracking since we're manually setting time
accumulatedDriftMicros = 0;
Serial.print(F("Time set to: "));
Serial.print(hour);
Serial.print(F(":"));
if (minute < 10) Serial.print(F("0"));
Serial.print(minute);
Serial.print(F(":"));
if (second < 10) Serial.print(F("0"));
Serial.println(second);
// Verify the setting worked
Serial.print(F("Verification - H:M:S = "));
Serial.print(getHour());
Serial.print(F(":"));
Serial.print(getMinute());
Serial.print(F(":"));
Serial.println(getSecond());
}
// Serial command interface for debugging and calibration
void handleSerialCommands() {
if (Serial.available()) {
String command = Serial.readStringUntil('\n');
command.trim();
command.toLowerCase();
if (command == "cal" || command == "calibrate") {
calibrateClockAccuracy();
}
else if (command == "acc" || command == "accuracy") {
Serial.print(F("Clock accuracy: "));
if (totalRunTimeMicros > 60000000UL) {
Serial.print(getClockAccuracyPPM(), 3);
Serial.println(F(" PPM"));
} else {
Serial.println(F("Insufficient runtime"));
}
}
else if (command == "time") {
Serial.print(F("Current time: "));
Serial.print(getHour());
Serial.print(F(":"));
Serial.print(getMinute());
Serial.print(F(":"));
Serial.print(getSecond());
Serial.print(F("."));
Serial.println(getMillisecond());
}
else if (command.startsWith("set ")) {
// Format: "set HH:MM:SS"
int colonPos1 = command.indexOf(':', 4);
int colonPos2 = command.indexOf(':', colonPos1 + 1);
if (colonPos1 > 0 && colonPos2 > 0) {
uint8_t hour = command.substring(4, colonPos1).toInt();
uint8_t minute = command.substring(colonPos1 + 1, colonPos2).toInt();
uint8_t second = command.substring(colonPos2 + 1 ).toInt();
setTime(hour, minute, second);
} else {
Serial.println(F("Invalid format. Use: set HH:MM:SS"));
}
}
else if (command == "help") {
Serial.println(F("\nAvailable commands:"));
Serial.println(F(" cal - Enter calibration mode"));
Serial.println(F(" acc - Show current accuracy"));
Serial.println(F(" time - Show current time"));
Serial.println(F(" set HH:MM:SS - Set time"));
Serial.println(F(" help - Show this help"));
}
else {
Serial.println(F("Unknown command. Type 'help' for available commands."));
}
}
}
Wishing a great and productive week to all of you.




