MEGA 2560 LCD Keypad Shield Projects

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.

Thanks for sharing,

+1 for the schematics in the code :slight_smile:

1 Like