Smart WLED Clock with RTC, PIR, Audio feedback and Environmental Monitoring

Published  May 14, 2026   0
Smart WLED Clock with RTC, PIR, Audio feedback and Environmental Monitoring

The integration of addressable LEDs with the WLED firmware has revolutionized DIY lighting projects, allowing for complex visual effects with minimal to no coding.  However, most standard WLED-based clocks face significant limitations, such as time-drift during internet outages and continuous power consumption regardless of room occupancy and no alarms. To address these issues, this project demonstrates the construction of an advanced feature rich smart clock.

Features

  1. Set RGB Colours
  2. Temperature
  3. Humidity
  4. Dusk Sensor
  5. Motion Detection
  6. 5 Audio Alarms
  7. Theft Detection
  8. 1 Theft Alarm Audio
  9. Buzzer 
  10. Battery Backup
  11. Fully Programmable
  12. RTC Support
  13. Wifi Connectivity
  14. Configure Via Web App
  15. IR Remote Control
  16. Power Saving Mode

Components Required 

The following components are required to build this advanced smart clock. It is recommended to use high-quality modules to ensure long-term stability.

ComponentSpecificationQuantity
MicrocontrollerESP32 Wroom -32 - 4 Mb1
LED Matrix/StripWS2812B Addressable RGB LEDs 73 LEDs
RTC ModuleDS3231 (with CR2032 Battery)1
Motion SensorPIR Sensor (MH-SR 602 or HC-SR501)1
Temp/Hum SensorDHT11 Sensor1
IR ReceiverTSOP1838 or equivalent1
Capacitor SMT16v/100 uf 1
Capacitor SMD100uf (0805)
22uf (0805)
8
2
Resistor10k (0805)
1k (0805)
5.1K (0805)
10
5
2
LDR22R (0805)
10k or 100k (0805)
1
1
LEDsAny Colour (0805)4 pcs
JST Connector2.54mm 6 Pin JST THT Male & Female1 each
Power Path ICIP5306_IC1
USB To TTLCH340C_IC1
TransistorBC817 or equivalent1
Sound ModuleJQ6500 or DF player

1

BuzzerGeneric THT Buzzer 12mm x 9mm 3.3/5v1
LDO RegulatorAMS 1117 3.3 v LDO1
RelayHF46F 5V DC Relay1
USB portType C 16-P SMD 
Type A 4-P SMD

1

1

Digital transistorUMH11N or similar1
Buttons 4 pin smd 6mm x 6mm x 6mm7
Battery18650 lithium-ion battery with holder1 each
SpeakerMylar speaker 0.5W 8Ohm 40mm (4-ohm 3 watt Recommended)1
Power Supply5V 3A DC Adapter1
Enclosure3D Printed (PLA/PETG)1
PCBPrinted Circuit board2 main+sensor

Circuit Diagram

Full Circuit Diagram of Smart WLED Clock

Power and Programming Input

16 pin Type-C Port for programming and powering the device

We are using a 16-pin type c port as it offers reciprocal, a reversible connection for data and powering.

Let's see how connections are made.

1. Power Supply (VBUS & GND)

  • VBUS (Pins 2 & 11): These are the positive power lines. They are tied together and labeled here as 5VC, providing the 5V power 
  • GND (Pins 1 & 12): These are the ground pins, connected to the common ground.
  • SHELL (Pins 13 & 14): The metal outer casing of the connector is also grounded to provide shielding and physical stability.

2. USB 2.0 Data (UD + & UD -)

  • DP1/DP2 (Pins 6 & 8): Data Plus. These are bridged together to the UD+ label.
  • DN1/DN2 (Pins 7 & 5): Data Negative. These are bridged together to the UD- label.
  • Why are they bridged? Since USB-C is reversible, bridging the DP and DN pairs ensures that data connectivity works regardless of which way you plug in the cable, similar for power.

3. Configuration Channel (CC1 & CC2)

  • Pins 4 & 10: These are the most critical pins for USB-C. In this diagram, they are pulled to VBUS through 5.1k resistors (R18 & R19) this makes sure type c to type c cable provide 5 v power. Missing of those resistors will only work for type A to Type c cable.

USB to TTL Converter

CH340C USB To TTL IC

CH340C USB-to-UART (TTL) Serial Converter circuit. is the "bridge" that allows your computer to talk to the ESP32 chip. When you plug your clock into a PC to upload the firmware, this chip converts the USB signals into a language the ESP32 understands.

Here is the breakdown of how this circuit functions:

1. USB Interface (The Left Side)

  • UD+ and UD-: These connect directly to the UD+ and UD- pins we saw on your USB-C connector. This is where the raw USB data enters the chip.
  • GND: Common ground for signal stability.

2. Power and Decoupling (Top and Right)

  • VCC (Pin 16): Connected to +3V3. This chip is being powered by a 3.3V regulator (standard for ESP32 projects).
  • Decoupling Capacitors: You see two sets of capacitors (100 uf  and 0.1\uf ) in parallel.
    • The 100uf capacitor acts as a reservoir to handle power spikes.
    • The 0.1uf capacitor filters out high-frequency noise.
    • Note: The diagram shows them doubled up; usually, one of each is sufficient right next to Pin 16.

3. UART Serial Data (TXD and RXD)

  • TXD (Pin 2): Transmit Data. This sends data from the computer to the ESP32's RX pin.
  • RXD (Pin 3): Receive Data. This receives data from the ESP32's TX pin.
  • 1k\ohm Resistors (R21, R22): These are inline protection resistors. They prevent "back-powering" the chips and protect against short circuits if two pins accidentally try to output data at the same time.

4. Auto-Reset Circuit (RTS and DTR)

  • RTS_RESET and DTR (Pins 14 & 13): These are the "handshake" pins.
  • In the world of ESP32/Arduino, these pins are used to automatically put the ESP32 into Bootloader Mode.
  • When you click "Upload" in your IDE, the computer toggles these pins to reset the ESP32 and pull GPIO 0 low, so you don't have to manually press buttons on the board to flash the firmware.

Power Management Circuit

circuit shows IP5306 ic used for charging battery and providing constant 5V Supply

This schematic features the IP5306, a highly integrated multi-function power management SOC (System on Chip) that is the "Power Bank" circuit. It allows the clock to run off an 18650 Li-ion battery, charges that battery when USB is plugged in, and boosts the battery voltage to a stable 5V for the LEDs and ESP32.

Digital Alarm Clock Part Marking

Here is the breakdown of the circuit sections:

1. Battery Charging & Power Input

  • VIN (Pin 1): Receives 5V power from your USB-C connector (5VC). This charges the connected battery.
  • BAT (Pin 6): Connects to the positive terminal of the 18650 battery. The IP5306 manages the charging cycle (Constant Current/Constant Voltage) to ensure the battery is charged safely.
  • C12, C13 (Input Caps): Filter the incoming USB power to prevent voltage ripples.

2. Synchronous Boost Converter (The 5V Output)

  • VOUT (Pin 8): This is the boosted output labeled 5VBOOST. When the USB is unplugged, the chip takes the battery's approx 3 to 4.2 V and boosts it to a steady 5V.
  • SW (Pin 7) & L1 (1uH Inductor): These are the core components of the switching regulator. The inductor stores energy in a magnetic field to push the voltage up to 5V.
  • Output Capacitors (C1 to C5): A massive bank of capacitors to ensure the 5V supply is extremely stable, which is critical because addressable LEDs (WS2812B) are very sensitive to power fluctuations.

3. Monitoring & Interaction

  • LED1–LED4 (Pins 2, 3, 4): These pins drive the four LEDs on the left. They provide a visual "Fuel Gauge," showing battery capacity in increments of 25%, 50%, 75%, and 100%.
  • KEY (Pin 5): Connected to a momentary push-button.
    • Short press: Wakes up the chip and displays battery level.
    • Double press: Usually turns the output off (enters standby/low power mode).
  • USB Plugged In: 5VC => IP5306 =>Charges Battery AND output 5V. powers the Clock
  • USB Unplugged: Battery => IP5306 (Boosts) => 5V powers the Clock.f

Power Switch and Voltage Regulator

circuit shows a power switch and AMS 1117 3.3 V linear Regulator

The power management stage is designed to convert the incoming 5V supply into a stable 3.3V rail required for the ESP32 Microcontrolller

1. Input Selection and Switching (PWR_SW1)

The circuit uses a multi-pin slide switch (PWR_SW1) to control the power flow. When the switch is toggled, it bridges the input to the internal +5V rail, effectively acting as the main power switch for the entire device.

2. Low Dropout Regulator (AMS1117-3.3)

To derive the 3.3V rail, an AMS1117-3.3 LDO (Low Dropout Regulator) is utilized. This is a common choice in ESP32 projects due to its ability to provide up to 800mA of current, which is sufficient to handle the power spikes during Wi-Fi transmission.

  • Pin 3 (VIN): Receives the switched 5V input.
  • Pin 2 & 4 (VOUT): Provides the regulated 3.3V output.
  • Pin 1 (GND/ADJ): Connected to the common ground to establish the voltage reference.

3. Filtering and Decoupling Stage

To ensure the output is free from high-frequency noise and voltage ripples (which can be caused by the fast switching of the WS2812B LEDs or IP5306 ic ), a robust filtering network is implemented:

  • Bulk Capacitance: Two large 220\uF electrolytic capacitors (labeled C5V and C1V) are placed on the 5V input line. These act as energy reservoirs to prevent voltage drops when the load suddenly increases.
  • High-Frequency Decoupling: 0.1\uf ceramic capacitor (C1) is placed on the 3.3V output line. This capacitor is critical for filtering out low-amplitude, high-frequency noise that the larger electrolytic capacitors cannot catch, ensuring the ESP32 receives a clean signal.

Boot and Reset Circuit

Boot and Reset Circuit

1. Boot & Reset Buttons (Manual Control)

This is the "fail-safe" part. It allows you to manually interact with the ESP32 hardware.

  • RST Button: Connected to the ESP32's EN (Enable/Reset) pin. When pressed, it shorts the pin to GND, forcing the chip to restart.
  • BOOT Button: Connected to GPIO0. For an ESP32 to accept new code, GPIO0 must be held at GND while the chip resets.
  • Pull-up Resistors (RR1, RB1 - 10k\Ohm) These are vital. They tie the pins to +3V3 by default. This ensures the chip stays "On" and in "Run Mode" unless you explicitly press a button to pull the signal to Ground.

2. UMH11N Digital Transistor (Automatic Control)

The UMH11N is a specialized dual-transistor package. It automates the "Button Mashing" required to flash firmware. It takes the RTS_RESET and DTR signals from the CH340C chip we discussed earlier and converts them into Reset and Boot commands.

How the Logic Works:

The transistors act as electronic switches. They are wired in a specific cross-coupled configuration often called the "ESP32 Auto-Program Circuit":

  • Automated Upload: When you click "Upload" in your browser or IDE, the CH340C toggles RTS and DTR. This circuit translates those signals to pull the RST and BOOT lines low in the correct sequence.
  • The Benefit: You don't have to open the 3D-printed enclosure or hold down tiny buttons every time you want to update your Code; the computer handles it all through the USB-C cable.

5 V Output for External Use

5 V Output for External use

 

Circuit Breakdown

  • 5VBOOST (Pin 1): This pin is connected directly to the output of your IP5306 Power Management IC. Since the IP5306 is a boost converter, it can provide a steady 5V even when the clock is running solely on its internal 18650 battery.
  • GND (Pin 4): Connected to the common system ground to complete the circuit.
  • Data Pins (Pins 2 & 3): These are marked with green "X" symbols, meaning they are Not Connected (NC). Because there are no data lines, this port cannot transfer files; it is strictly for "dumb" charging or powering devices in an emergency.
  • Mechanical Shielding (Pins 5 & 6): These represent the metal housing of the USB-A connector, typically soldered to the PCB for physical strength and grounded to reduce electromagnetic interference.

ESP32 Microcontroller Circuit

ESP32-WROOM-32 (4MB) microcontroller and pin Connections

Pin Mapping & Peripheral Connectivity

1. Communication Interfaces

  • I2C Bus: 
  • SDA: IO21 (Pin 33)
  • SCL: IO22 (Pin 36)
  • UART:  TXD0/RXD0: Standard programming/debug port (Pins 34, 35).
  • TX2/RX2: Secondary UART interface on IO17 and IO16 (Pins 27, 28).
  • SPI (Flash/Internal): Pins 17–22 is reserved for the internal flash memory connection (SD2, SD3, CMD, CLK, SD0, SD1).

2. Sensors & Input

  • LDR (Light Dependent Resistor): Connected to SENSOR_VN (Pin 5) for analog light sensing.
  • DHT (Temp/Humidity): Assigned to IO18 (Pin 30).
  • PIR (Motion): Assigned to IO33 (Pin 9).
  • IR (Infrared): Connected to IO13 (Pin 16).

Navigation Buttons:

  • UP: IO26 (Pin 11)
  • DOWN: IO27 (Pin 12)
  • OK: IO14 (Pin 13) f

3. Outputs & Indicators

  • STRIP: IO23 (Pin 37) connected to Relay Circuit for cutting power to LEDs
  • WS2812B: IO4 (Pin 26) Data pin for LEDs
  • Buzzer (BUZ): Connected to IO12 (Pin 14).
  • Status LED: A red 0603 LED (LED69) is connected to IO2 (Pin 24) through a 1k\Ohm current-limiting resistor (RLED).
  • SQW: A square wave output signal (goes to DS3231 RTC) is connected to IO19 (Pin 31).

4. System Control

  • BOOT: Pin IO0 (Pin 25) is broken out for flashing mode selection.
  • EN (Reset): The RST line (Pin 3) handles the module reset logic.
  • CFG: A configuration pin assigned to IO15 (Pin 23).
  • BUSY: A status feedback pin on IO25 (Pin 10).

Sound Module Circuit

JQ6500 & DF PLAYER used for playing mp3 files

We have designed the PCB footprint to be compatible with both modules, allowing us to choose the most available or cost-effective option during assembly.

  • Shared Control: Both modules use a UART interface (TX2/RX2). We included 1k/ohm resistors on the data lines to protect the modules from 5V logic spikes and reduce serial noise.
  • Dual-Compatible Audio: The speaker outputs from both the JQ6500 (SPK+/-) and the DFPlayer (SPK1/2) are routed to a single 2-pin JST connector. This ensures that regardless of which Module we solder, the audio reaches the speaker.
  • Status Monitoring: We tied the BUSY pins together. This lets our code monitor if the module is currently playing audio, regardless of which hardware is used.
  • Power: Both footprints are tied to a regulated +5V rail with a shared ground to maintain signal integrity.

NOTE: Adding custom audio in jq6500 

You will need 6 Audios, Audio 1,2,3,4,5 corresponds to 5 Alarm and Audio 6 is for Theft Alarm. All are in mp3 format, and all combined audio size should be below or up to 2000kb / 2 mb, most important is to enter audio name as 001 - 002 - 003 … where audio 1 is 001 and audio 2 is 002 and so on. Now you can Upload it in jQ6500 Module.

RTC Circuit

DS3231 Real-Time Clock (RTC) module

We use this to maintain highly accurate timekeeping for the system, even if the main power is lost.

Connections:

  • I2C Communication: We use the SDA (Pin 3) and SCL (Pin 4) lines to interface with the ESP32 for time data transfer.
  • Square Wave (SQW): Pin 5 is connected to the SQW net. We can use this to trigger precise 1Hz interrupts or as an alarm output.
  • Power: The module is powered by the +3V3 rail (Pin 2) with a common GND (Pin 1).
  • 32K Output: Pin 6 (32KHz output) is left unconnected (marked with an 'X'), as we don't require that specific frequency for this design.

Control Buttons

4 control buttons for device settings

Control Button array used for user input (UP, DOWN, OK, CFG). We have implemented an Active-High logic configuration for these tactile switches.

How it Works

  • Pull-Down Resistors: We use 1k\Ohm resistors (R3, R4, R14, R16) connected to GND. This ensures the signal lines stay at 0V (Low) when the buttons are not pressed, preventing "floating" pins that could cause false triggers.
  • Switching Logic: One side of each switch is connected to the +3V3 rail.
  • Triggering: When we press a button, it closes the circuit and pulls the corresponding GPIO pin (UP, DOWN, OK, or CFG) to 3.3V (High).

Relay Circuit

relay driver circuit using BC817 Transistor & HF46F 5 V Relay

Relay Driver circuit allows a low-power microcontroller signal to switch a high-power load using a BC817 NPN transistor.

How it Works

  • Switching Mechanism: We apply a signal to the STRIP net. This current passes through the 1k\Ohm base resistor (R17) to the transistor, which then acts as a switch, connecting the relay coil to GND.
  • Flyback Protection: We've included an M7 diode in parallel with the relay coil. This is crucial as it protects our transistor from high-voltage spikes (back-EMF) generated when the relay coil is deactivated.
  • Power: The relay coil is powered by a +5V rail, while the contacts (Pins 2 and 3) are set up to switch a separate VCC load when the relay is energized.
  • We are using relay here to cut power to LEDs so that WS2812 ICs inside leds don't consume power when leds are not lit.

WS2812B LEDs Circuit

73 WS2812B LEDs connected in Daisy chain

This schematic features an addressable WS2812B RGB LED matrix consisting of 73 individual LEDs. We’ve designed this for a high-density visual display, for a clock face 

Key Design Features

  • Daisy-Chain Configuration: We’ve connected the LEDs in a serial data line. The DOUT (Data Out) of one LED feeds directly into the DIN (Data In) of the next. This allows us to control all 73 LEDs using just a single GPIO pin from the ESP32.
  • Power Distribution: All LEDs share a common VCC and GND rail. Since each WS2812B can draw up to 60mA at full white brightness, we must ensure our power supply can handle roughly 4.4A 73x60 Ma for this board.
  • Connectors: We included 3-pin headers at the start (DIN) and end (DOUT) of the chain. This makes it easy to link multiple PCBs together or inject power at different points to prevent voltage drop.

NOTE: Because we have 73 LEDs in a tight "snake" pattern, we should include decoupling capacitors (typically 100nF) near the VCC pin of each LED on the PCB layout to filter out switching noise and keep the data signal clean across the entire chain. And the power consumption can reach up to 5 amps so we will set max brightness to 50 percent to avoid power failure.

Sensors Circuit

MHSR602 PIR sensor along with DHT11 , TSOP1838 & Analog GL5516 LDR

1. Sensor Connector (S6B-XH-A-1) 2.54 x 6 pin

We have included a 6-pin JST-XH connector as a centralized interface for external Main clock Board.

Pins: PIR, LDR, IR, DHT, +3V3, and GND.

2. Analog LDR (Light Detection)

We are using a GL5516 photoresistor in a voltage divider circuit with a 10k\Ohm resistor (R27).

Operation: As the light intensity changes, the resistance of the LDR varies, altering the voltage at the LDR net. This analog signal is then read by the ESP32’s ADC to determine ambient brightness, controlling led brightness accordingly.

Parts Marked Both Front and Back Side of the Digital Clock

3. PIR Sensor (Motion Detection)

The design utilizes an MH-SR602, which is a compact, high-sensitivity passive infrared sensor.
Connectivity: It operates on +3V3 and sends a digital high trigger to the PIR net whenever motion is detected.

4. DHT11 (Climate Sensing)

For temperature and humidity, we’ve integrated the DHT11.

  • Data Line: It uses a single-bus digital protocol connected to the DHT net. We’ve powered it via the +3V3 rail to ensure compatibility with our logic levels.

5. TSOP 1838 (Infrared Receiver)

To enable remote control functionality, we included the TSOP4838/1838 IR receiver.

Filtering: We added a 10\uf decoupling capacitor (C2) across the power pins (VS and GND). This is critical for filtering out power supply noise, which ensures the IR data signal remains clean and accurate for decoding.

Arrangement of LEDs

placement of LEDs for clock face and  DATA IN  & DATA OUT position

Description: The 2-layer PCB layout highlighting the optimized routing for WS2812b leds in Daisy chain showing START & END ie  Data in AND Data Out

Back Side of PCB

Arrangement of all components on a pcb

PCB Layout and Routing:
Description: Arrangement of ESP32 and all other components like RTC, Battery holder, type C port, etc.

3D View of Front and Back Side Of PCD

Real World Image After Assembly

Back of PCB

3D view of front of PCB

Soldering and Assembly

Description: The PCB during the assembly phase. Surface-mount components were soldered first, followed by through-hole modules like the DS3231 RTC and the PIR sensor to maintain a low-profile form factor.

NOTE: There is space for 2 Capacitor near Relay they are connected parallel to support high current surge, but soldering 2 of them was shorting out the IP5306 5v output damaging it permanently. hence the yellow wire jumper seen in image is used to recover burnt track due to short (using single capacitor works flawlessly but the clock will reboot whenever external power is disconnected)

3D View Of CAD Files

3D view of Digital Clock

Programming / STL / Gerbers

All programming files, gerbers, stl  and Instructions are posted on GitHub repository of Smart WLED Clock . Code is written in ARDUINO IDE and is inspired from WLED open-source code. PCB Designed in EASY_EDA and 3D Modelling in ONSHAPE

GitHub repository of Smart WLED ClockSmart WLED Clock Zip File

Setting up the clock after programming.

  1. After successful programming RESTART Clock. 
  2. Press CONFIG Button once, it will open Access Point (AP Mode)
  3. Connect to Access Point “Clock-EFXXXXX”
  4. It will open WEB UI For clock config (if didnt just open Wled app)
  5. Set all parameter of your choice 
  6. And connect clock to Home WiFi 
  7. Click save & exit wifi settings (ONLY then This will close AP Mode) And connect to given wifi network
  8. Now connect your mobile to same home wifi and open WLED App, click (+) Icon and you will see Same name as “Clock-EFXXXXX”, Click Add, now it will fetch time automatically from selected NTP Server and you can set all parameters anytime without reaching clock from App itself.
  9. If by mistake wrong WiFi Credentials are given, you can press Config button again any time to open Access Point Mode.

Overall Look and Web UI

Features of Digital Alarm Clock

 

Complete Project Code

//parition scheme : Minimal SPIFFS
/*
* Auther : Vishal Soni (@electrodonut)
* ESP32 Board : V3.3.4
* FastLED : v3.10.3
* RTClib : v2.1.4
* DHT sensor Adafruit : v1.4.6
* JQ6500_Serial : (Link: https://github.com/sleemanj/JQ6500_Serial)
*/
/*++++++++++++++ Libraries ++++++++++++++++*/
#define FASTLED_ALLOW_INTERRUPTS 1
#define FASTLED_INTERRUPT_RETRY_COUNT 0
#include <Arduino.h>
#include <FastLED.h>
#include <Wire.h>
#include "RTClib.h"
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WebServer.h>
#include <DNSServer.h>
#include <Preferences.h>
#include <ArduinoJson.h>
#include "DHT.h"
#include <JQ6500_Serial.h>
#include <Update.h>

#include "webpage.h"
#include "timezone.h"
/*++++++++++++++ Wifi ++++++++++++++++*/
String wifiSSID;
String wifiPassword;
WebServer server(80);
Preferences prefs;
Preferences wifiPrefs;
char mdnsHostname[32];
// Timezone handling
String tzString;  // Will hold POSIX timezone string
bool wifiConnected = false;
bool timeSynced = false;
unsigned long lastUpdateTime = 0;
bool apModeActive = false;
bool mdnsRunning = false;
bool serverRunning = false;

DNSServer dnsServer;
// const char* AP_PASSWORD = "12345678";  // ≥8 chars or set nullptr for open AP
const char *AP_PASSWORD = nullptr;
;  // ≥8 chars or set nullptr for open AP
 
/*++++++++++++++ Pin Declaration ++++++++++++++++*/
#define LED_PWR_RELAY 23
#define DATA_PIN 4
#define DHTPIN 18  // Digital pin connected to the DHT sensor
#define LDR 39
#define PIR 33
#define BUSY_AUDIO 25  //1 busy 0 free
#define UP_BUTTON 26
#define DOWN_BUTTON 27
#define OK_BUTTON 14
#define CONFIG_BUTTON 15
/*++++++++++++++ Display ++++++++++++++++*/
#define NUM_LEDS 73
CRGB leds[NUM_LEDS];         // Define the array of leds
int display_cycle_time = 3;  //seconds
unsigned long lastTimeDisplaySwitch = 0;
int Display_Value = 0;
#define MAX_DISPLAY_VALUES 3
String displayOnly;  // "all", "time", "temperature", "humidity"
/*++++++++++++++ Buttons ++++++++++++++++*/
const unsigned long debounceDelay = 50;  // ms
struct Button {
 uint8_t pin;
 bool lastStableState;
 bool lastReading;
 unsigned long lastDebounceTime;
 const char *name;
};
Button buttons[] = {
 { UP_BUTTON, LOW, LOW, 0, "UP" },
 { DOWN_BUTTON, LOW, LOW, 0, "DOWN" },
 { OK_BUTTON, LOW, LOW, 0, "OK" },
 { CONFIG_BUTTON, LOW, LOW, 0, "CONFIG" }
};
const int buttonCount = sizeof(buttons) / sizeof(buttons[0]);
/*++++++++++++++ Sound define ++++++++++++++++*/
#define Alarm1Sound 1
#define Alarm2Sound 2
#define Alarm3Sound 3
#define Alarm4Sound 4
#define Alarm5Sound 5
#define TheftAlarmSound 6
uint8_t alarm_volume = 30;
/*++++++++++++++ Alarm System ++++++++++++++++*/
// Alarm structure for 5 alarms
struct Alarm {
 bool enabled;
 int hour;
 int minute;
 int sound_num;
 unsigned long snooz_minute;
 int back_up_h;
 int back_up_m;
};
#define ALARM_COUNT 5
Alarm alarms[ALARM_COUNT] = {
 //true/false(enable/disale) , hour , minute, sound number, snooz minute, backup hour and backup minte?
 { false, 7, 0, Alarm1Sound, 1, 7, 0 },    // Alarm 1 - default 7:00 AM snooze 1 minute
 { false, 8, 0, Alarm2Sound, 10, 8, 0 },   // Alarm 2 - default 8:00 AM snooze 10 minute
 { false, 9, 0, Alarm3Sound, 5, 9, 0 },    // Alarm 3 - default 9:00 AM snooze 5 minute
 { false, 10, 0, Alarm4Sound, 0, 10, 0 },  // Alarm 4 - default 10:00 AM
 { false, 13, 0, Alarm5Sound, 0, 13, 0 }   // Alarm 5 - default 1:00 PM
};
bool alarm_is_active = 0;
bool is_snooz_pressed = 0;
int current_active_alarm = 0;
/*++++++++++++++ PIR ++++++++++++++++*/
bool first_time_on_before_pir = 0;
bool is_pir_on = 1;
bool turn_on_relay = 0;
int time_out_for_led_off = 1;  // 1 minute on
// int time_out_for_led_off = 1000 * 5 * 1;  // 1 minute on
unsigned long lastTimePirOn = 0;
JQ6500_Serial mp3(Serial2);
bool is_theft_on = 0;
int theftSoundTimeout;  // seconds
bool theftTimingEnabled;
struct TheftTime {
 uint8_t hour;
 uint8_t minute;
} theftStartTime, theftEndTime;
bool theftTimingInRange = 0;  //if in range turn it 1
bool Theft_active = 0;        //whill 1 if theft audio playing
/*++++++++++++++ LDR ++++++++++++++++*/
bool autoBrightness = 1;
int minRawValue = 4095;  // Expected in complete darkness
int maxRawValue = 1425;  // Expected in bright light
int minLedBrightness = 10;
int maxLedBrightness = 255;
int rawValue = 0;
int brightness;
// int lightLevel = 0;
/*++++++++++++++ DHT 11 ++++++++++++++++*/
bool is_temp_in_f = 0;
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
float humidity = 0;
float temperature = 0;
/*++++++++++++++ LEds ++++++++++++++++*/
// Define segments as reusable objects
CRGBSet hour_1(leds + 0, 14);   // First hour digit (LEDs 0-13, 14 LEDs total)
CRGBSet hour_2(leds + 14, 14);  // Second hour digit (LEDs 14-27, 14 LEDs)
CRGBSet sep_dot(leds + 28, 1);  // Separator dot (LED 28 only, 1 LED)
CRGBSet min_1(leds + 29, 14);   // First minute digit (LEDs 29-42, 14 LEDs)
CRGBSet min_2(leds + 43, 14);   // Second minute digit (LEDs 43-56, 14 LEDs)
CRGBSet sun(leds + 57, 2);
CRGBSet sat(leds + 59, 2);
CRGBSet fri(leds + 61, 2);
CRGBSet thu(leds + 63, 2);
CRGBSet wed(leds + 65, 2);
CRGBSet tue(leds + 67, 2);
CRGBSet mon(leds + 69, 2);
CRGBSet al(leds + 71, 1);
CRGBSet pm(leds + 72, 1);
unsigned long currentMillis = 0;
unsigned long lastPrintTime = 0;
// Animation variables
uint8_t gHue = 0;  // Global hue for rainbow
/*++++++++++++++ Time ++++++++++++++++*/
RTC_DS3231 rtc;
uint8_t hours = 2;
uint8_t minutes = 2;
uint8_t seconds = 0;
uint8_t date = 5;
uint8_t month = 10;
int year = 2001;
uint8_t weekday = 0;
uint8_t lastweekday = 255;
char daysOfWeek[7][12] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
bool enable_24_hour_mode = 0;  //make 1 to enable 24 hour mode
bool is_pm = 0;                //will 1 if pm
String timezone;
String timeMode;  // "12" or "24"
CRGB weekday_bg_color = CRGB::White;
CRGB weekday_selected_color = CRGB::Magenta;
/*++++++++++++++ Colors Solid ++++++++++++++++*/
String colorMode;
String solidColor;
String effect;
enum color_mode {
 SOLID,
 EFFECT
};
enum color_mode Color_Mode = EFFECT;
enum color_of_time {
 RED,  // Basic colors
 GREEN,
 BLUE,
 WHITE,
 BLACK,   // Off
 YELLOW,  // Secondary colors
 CYAN,
 MAGENTA,
 ORANGE,  // Tertiary colors
 PURPLE,
 PINK,
 TEAL,
 LIME,
 AMBER,  // Warm colors
 GOLD,
 CORAL,
 LAVENDER,  // Pastels
 SKYBLUE,
 FORESTGREEN,
 MAROON,
 OLIVE,
 NAVY,
 INDIGO
};
enum color_of_time current_color = MAROON;
/*++++++++++++++ effects of time ++++++++++++++++*/
enum effect_of_time {
 RAINBOW_1,  // slow rainbow
 RAINBOW_2,
 RAINBOW_3
};
enum effect_of_time current_effect = RAINBOW_2;
/* ---- Theme ---- */
bool lightTheme;
// ===================== declaration =====================
long getUtcOffsetSeconds(const String &tz);
// ===================== Setup() =====================
void setup() {
 Serial.begin(115200);
 Serial.println("========================== Beginning of Code ============================");
 loadPreferences();
 loadWiFiCredentials();
 WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str());
 update_hostname();
 // FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);  // GRB ordering is typical
 FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
 Wire.begin();  // Initialize I2C with default pins (21-SDA, 22-SCL)
 // Initialize RTC
 if (!rtc.begin()) {
   Serial.println("Couldn't find RTC!");
   Serial.flush();
   while (1) delay(10);
 }
 dht.begin();
 Serial2.begin(9600, SERIAL_8N1, 16, 17);
 mp3.reset();
 mp3.setVolume(alarm_volume);
 mp3.setLoopMode(MP3_LOOP_NONE);
 pinMode(BUSY_AUDIO, INPUT);
 pinMode(LDR, INPUT);
 pinMode(PIR, INPUT);
 pinMode(LED_PWR_RELAY, OUTPUT);
 digitalWrite(LED_PWR_RELAY, HIGH);
 turn_on_relay = 1;
 // server_start();
 // test alarm
 // DateTime now = rtc.now();
 // hours = now.hour();
 // minutes = now.minute();
 // seconds = now.second();
 // set_alarm(1, true, hours, minutes + 1, Alarm1Sound, 1);  //alarm number(1-5), enable(true-false), hour, minute, sound, snooze minute
}
// ===================== Loop() =====================
void loop() {
 server.handleClient();
 if (apModeActive) {
   dnsServer.processNextRequest();
 }
 currentMillis = millis();
 button_handler();
 PIR_handle();
 updateTime();
 handle_alarm();
 PrintDebug();
 WifiHandle();
 UpdateDHT();
 // EVERY_N_MILLISECONDS(33) {  // ~30 FPS
 if (turn_on_relay) {
   autoBrightness_handle();
   // FastLED.setBrightness(255);
   display_weekday();
   Alarm_led_handle();
   if ((currentMillis - lastTimeDisplaySwitch > (display_cycle_time * 1000)) && displayOnly == "all") {
     lastTimeDisplaySwitch = currentMillis;
     Display_Value = (Display_Value + 1) % MAX_DISPLAY_VALUES;
     clearAllSegments_NoWeek();
   }
   if (Color_Mode == SOLID) {
     if (Display_Value == 0) {
       displayTime((uint8_t)current_color, Color_Mode);
     } else if (Display_Value == 1) {
       displayHumidity((uint8_t)current_color, Color_Mode);
     } else if (Display_Value == 2) {
       displayTemperature((uint8_t)current_color, Color_Mode);
     }
   } else {
     if (Display_Value == 0) {
       displayTime((uint8_t)current_effect, Color_Mode);
     } else if (Display_Value == 1) {
       displayHumidity((uint8_t)current_effect, Color_Mode);
     } else if (Display_Value == 2) {
       displayTemperature((uint8_t)current_effect, Color_Mode);
     }
     // Update animation based on effect speed
     if (current_effect == RAINBOW_1) {
       gHue++;            // Update hue for animation
       FastLED.delay(5);  // Fast animation
     } else if (current_effect == RAINBOW_2) {
       gHue++;             // Update hue for animation
       FastLED.delay(15);  // Medium animation
     } else if (current_effect == RAINBOW_3) {
       gHue++;             // Update hue for animation
       FastLED.delay(30);  // Slow animation
     }
   }
   FastLED.show();
 }
 // }
}
// ===================== Button Handler =====================
void button_handler() {
 for (int i = 0; i < buttonCount; i++) {
   bool reading = digitalRead(buttons[i].pin);
   // If state changed, reset debounce timer
   if (reading != buttons[i].lastReading) {
     buttons[i].lastDebounceTime = millis();
   }
   // If debounce time passed, accept new state
   if ((millis() - buttons[i].lastDebounceTime) > debounceDelay) {
     if (reading != buttons[i].lastStableState) {
       buttons[i].lastStableState = reading;
       // Print only on press (HIGH)
       if (reading == HIGH) {
         Serial.print(buttons[i].name);
         Serial.println(" button pressed");
         switch (buttons[i].pin) {
           case UP_BUTTON:
             break;
           case DOWN_BUTTON:
             break;
           case OK_BUTTON:
             if (alarm_is_active) {
               is_snooz_pressed = 1;
             }
             break;
           case CONFIG_BUTTON:
             startAPMode();
             break;
           default:
             break;
         }
       }
     }
   }
   buttons[i].lastReading = reading;
 }
}
// ===================== Audio =====================
void stop_audio() {
 mp3.reset();
 mp3.setVolume(alarm_volume);
}
// ===================== Set Alarm =====================
void handle_alarm() {
 for (int i = 0; i < ALARM_COUNT; i++) {
   if (alarms[i].enabled) {
     if (alarms[i].hour == hours && alarms[i].minute == minutes && seconds == 0) {
       current_active_alarm = i;
       Serial.println("============================================");
       Serial.print("Alarm Started: ");
       Serial.println(current_active_alarm + 1);
       Serial.println("============================================");
       if (alarms[i].hour == alarms[i].back_up_h && alarms[i].minute == alarms[i].back_up_m) {
         reset_alarm_to_defult();  //this well remove any snooze times
       }
       if (is_pir_on && !turn_on_relay) {
         TurnOnDisplay();
         turn_on_relay = 1;
       }
       if (digitalRead(BUSY_AUDIO)) {
         stop_audio();
       }
       mp3.setLoopMode(MP3_LOOP_ONE_STOP);
       mp3.playFileByIndexNumber(alarms[i].sound_num);
       alarm_is_active = 1;
       Theft_active = 0;  //stop theft if alarm start
     }
   }
 }
 if (alarm_is_active) {
   //no snooze pressed & completed
   if (!digitalRead(BUSY_AUDIO)) {
     Serial.println("============================================");
     Serial.print("Alarm Stopped: ");
     Serial.println(current_active_alarm + 1);
     Serial.println("============================================");
     alarm_is_active = 0;
     if (is_pir_on && turn_on_relay) {
       turn_on_relay = 0;
       TurnOffDisplay();
     }
     reset_alarm_to_defult();
   }
   //snooze pressed
   if (is_snooz_pressed) {
     Serial.println("============================================");
     Serial.print("Alarm Snooze: ");
     Serial.println(current_active_alarm + 1);
     Serial.println("============================================");
     stop_audio();
     is_snooz_pressed = 0;
     alarm_is_active = 0;
     addMinutes(alarms[current_active_alarm].hour, alarms[current_active_alarm].minute, alarms[current_active_alarm].snooz_minute);
     if (is_pir_on) {
       turn_on_relay = 0;
       TurnOffDisplay();
     }
   }
 }
}
void reset_alarm_to_defult() {
 for (int i = 0; i < 5; i++) {
   alarms[i].hour = alarms[i].back_up_h;
   alarms[i].minute = alarms[i].back_up_m;
 }
}
void addMinutes(int &hours, int &minutes, int minutesToAdd) {
 // Convert everything to total minutes
 int totalMinutes = hours * 60 + minutes + minutesToAdd;
 // Handle rollover (24-hour format)
 totalMinutes = totalMinutes % (24 * 60);
 // Convert back to hours and minutes
 hours = totalMinutes / 60;
 minutes = totalMinutes % 60;
}
void set_alarm(uint8_t alarm_num, bool alarm_state, uint8_t alarm_hour, uint8_t alarm_minute, uint8_t alarm_sound, int snooze_minute) {  //alarm number(1-5), enable(true-false), hour, minute, sound, snooze minute
 alarm_num = alarm_num - 1;                                                                                                             //convert to array
 alarms[alarm_num].enabled = alarm_state;
 alarms[alarm_num].hour = alarm_hour;
 alarms[alarm_num].minute = alarm_minute;
 alarms[alarm_num].sound_num = alarm_sound;
 alarms[alarm_num].snooz_minute = snooze_minute;
 alarms[alarm_num].back_up_h = alarm_hour;
 alarms[alarm_num].back_up_m = alarm_minute;
}
void toggle_alarm(uint8_t alarm_num, bool alarm_state) {
 alarm_num = alarm_num - 1;  //convert to array
 alarms[alarm_num].enabled = alarm_state;
}
// ===================== Turn On Display =====================
void TurnOnDisplay() {
 digitalWrite(LED_PWR_RELAY, HIGH);
}
void TurnOffDisplay() {
 digitalWrite(LED_PWR_RELAY, LOW);
}
// ===================== PIR =====================
void PIR_handle() {
 if (theftTimingEnabled) {
   theftTimingInRange = is_time_between(hours, minutes, theftStartTime.hour, theftStartTime.minute, theftEndTime.hour, theftEndTime.minute);
 } else {
   theftTimingInRange = 1;
 }
 if (Theft_active && !digitalRead(BUSY_AUDIO)) {
   mp3.setLoopMode(MP3_LOOP_ONE);
   mp3.playFileByIndexNumber(TheftAlarmSound);
 }
 if (is_pir_on) {
   //first time on
   if (turn_on_relay && (currentMillis - lastTimePirOn) > (10 * 1000) && !Theft_active && !first_time_on_before_pir) {
     turn_on_relay = 0;
     TurnOffDisplay();
     first_time_on_before_pir = 1;
   }
   if (turn_on_relay && (currentMillis - lastTimePirOn) > (time_out_for_led_off * 1000 * 60) && !Theft_active) {
     if (is_theft_on && !alarm_is_active) {
       stop_audio();
     }
     turn_on_relay = 0;
     TurnOffDisplay();
   }
   //for theft
   if ((currentMillis - lastTimePirOn) > (theftSoundTimeout * 1000) && is_theft_on && Theft_active && !alarm_is_active) {
     stop_audio();
     Theft_active = 0;
     turn_on_relay = 0;
     TurnOffDisplay();
   }
   if (digitalRead(PIR) == HIGH && !turn_on_relay && !alarm_is_active) {
     if (is_theft_on && !alarm_is_active && theftTimingInRange) {
       Theft_active = 1;
     }
     TurnOnDisplay();
     turn_on_relay = 1;
     lastTimePirOn = currentMillis;
   }
   // if (turn_on_relay && is_theft_on && !digitalRead(BUSY_AUDIO)) {
   //   mp3.playFileByIndexNumber(TheftAlarmSound);
   // }
 } else {
   TurnOnDisplay();
   turn_on_relay = 1;
   if (digitalRead(PIR) == HIGH && !alarm_is_active && is_theft_on && theftTimingInRange) {
     Theft_active = 1;
     lastTimePirOn = currentMillis;
   }
   if ((currentMillis - lastTimePirOn) > (theftSoundTimeout * 1000) && is_theft_on && Theft_active && !alarm_is_active) {
     stop_audio();
     Theft_active = 0;
   }
 }
}
void checkRangeOfTheftTime() {
 bool theftTimingEnabled;
 struct TheftTime {
   uint8_t hour;
   uint8_t minute;
 } theftStartTime, theftEndTime;
 bool theftTimingInRange = 0;  //if in range turn it 1
 if (theftTimingEnabled) {
   theftTimingInRange = 0;
   if (theftStartTime.hour) {
   }
 } else {
   theftTimingInRange = 1;
 }
}
// Function to check if current time is between start and end times
int is_time_between(int current_hour, int current_minute,
                   int start_hour, int start_minute,
                   int end_hour, int end_minute) {
 // Convert all times to minutes since midnight
 int current_total = current_hour * 60 + current_minute;
 int start_total = start_hour * 60 + start_minute;
 int end_total = end_hour * 60 + end_minute;
 if (start_total <= end_total) {
   // Normal same-day case
   if (start_total <= current_total && current_total <= end_total) {
     return 1;
   }
 } else {
   // Next-day case (end time is on next day)
   if (current_total >= start_total || current_total <= end_total) {
     return 1;
   }
 }
 return 0;
}
// ===================== LDR =====================
float smoothedLightLevel = 0.0;  // Initial value
const float alpha = 0.2;         // Smoothing factor (0.1-0.3). Lower = smoother, slower response.
unsigned long lastLDRRead = 0;
const unsigned long LDR_READ_INTERVAL = 100;  // Read every 100ms
// Modify autoBrightness() function
void autoBrightness_handle() {
 if (autoBrightness) {
   if (currentMillis - lastLDRRead >= LDR_READ_INTERVAL) {
     lastLDRRead = currentMillis;
     rawValue = analogRead(LDR);
     // The LDR reading is INVERTED: high value = dark, low value = bright.
     int newLightLevel = map(rawValue, minRawValue, maxRawValue, minLedBrightness, maxLedBrightness);
     newLightLevel = constrain(newLightLevel, minLedBrightness, maxLedBrightness);
     // Apply Exponential Moving Average Filter
     smoothedLightLevel = (alpha * newLightLevel) + ((1 - alpha) * smoothedLightLevel);
     // Apply the smoothed brightness
     FastLED.setBrightness((int)smoothedLightLevel);
   }
 } else {
   int newLightLevel = map(brightness, 0, 100, minLedBrightness, maxLedBrightness);
   FastLED.setBrightness(newLightLevel);
 }
}

// ===================== Update DHT =====================
void UpdateDHT() {
 humidity = dht.readHumidity();
 if (!is_temp_in_f) {
   // Read temperature as Celsius (the default)
   temperature = dht.readTemperature();
 } else {
   // Read temperature as Fahrenheit (isFahrenheit = true)
   temperature = dht.readTemperature(true);
 }
 if (isnan(humidity) || isnan(temperature)) {
   Serial.println(F("Failed to read from DHT sensor!"));
   return;
 }
}
// ===================== DIGIT PATTERNS =====================
// Each digit pattern controls 14 LEDs (like two 7-segment digits)
// 1 = LED ON, 0 = LED OFF
const bool digitPattern[13][14] = {
 // Digit 0
 { 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1 },
 // Digit 1
 { 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1 },
 // Digit 2
 { 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0 },
 // Digit 3
 { 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1 },
 // Digit 4
 { 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 1, 1 },
 // Digit 5
 { 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1 },
 // Digit 6
 { 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1 },
 // Digit 7
 { 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1 },
 // Digit 8
 { 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 },
 // Digit 9
 { 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1 },
 // Digit 10 /H
 { 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1 },
 // Digit 11 /F
 { 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0 },
 // Digit 12 /C
 { 1, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0 },
};
// ===================== CORE FUNCTIONS =====================
void Alarm_led_handle() {
 al[0] = CRGB::Black;
 for (int i = 0; i < 5; i++) {
   if (alarms[i].enabled) {
     al[0] = CRGB::Red;
   }
 }
}
void display_weekday() {
 if (lastweekday != weekday) {
   // Serial.print("last week:");
   // Serial.print(lastweekday);
   // Serial.print(" ");
   // Serial.print("week:");
   // Serial.println(weekday);
   lastweekday = weekday;
   highlightDay(weekday_bg_color, weekday_selected_color, weekday);
 }
}
void displayDigit(CRGBSet &segment, int number, uint8_t time_effect_fun, enum color_mode Mode) {
 if (Mode == SOLID) {
   CRGB color = getColorFromEnum((color_of_time)(time_effect_fun));
   for (int i = 0; i < segment.size(); i++) {
     segment[i] = digitPattern[number][i] ? color : CRGB::Black;
   }
 } else {
   // FIXED: Corrected the logic for checking rainbow effects
   if (time_effect_fun == RAINBOW_1 || time_effect_fun == RAINBOW_2 || time_effect_fun == RAINBOW_3) {
     // Apply rainbow to entire segment
     fill_rainbow(segment, segment.size(), gHue, 7);
     // Mask out LEDs that shouldn't be lit according to digitPattern
     for (int i = 0; i < segment.size(); i++) {
       if (!digitPattern[number][i]) {
         segment[i] = CRGB::Black;
       }
     }
   }
   // Note: Remove gHue++ and FastLED.delay from here - update them in main loop
 }
}
void displayTime(uint8_t color, enum color_mode Mode) {
 uint8_t temp_hours = hours;
 uint8_t temp_minutes = minutes;
 if (!enable_24_hour_mode) {
   temp_hours = getTimeComponent(hours, minutes, 0);
   temp_minutes = getTimeComponent(hours, minutes, 1);
 }
 displayDigit(hour_1, temp_hours / 10, color, Mode);
 displayDigit(hour_2, temp_hours % 10, color, Mode);
 // Blinking colon with rainbow effect in EFFECT mode
 bool colonOn = (millis() / 500) % 2 == 0;
 if (Mode == SOLID) {
   // SOLID mode: use the color enum
   CRGB rgb_color = getColorFromEnum((color_of_time)color);
   sep_dot[0] = colonOn ? rgb_color : CRGB::Black;
   pm[0] = rgb_color;
 } else {
   // EFFECT mode: rainbow dot (when visible)
   if (colonOn) {
     // For rainbow effects, use gHue for the dot color
     sep_dot[0] = CHSV(gHue, 255, 255);
   } else {
     sep_dot[0] = CRGB::Black;
   }
   pm[0] = CHSV(gHue, 255, 255);
 }
 if (enable_24_hour_mode || !is_pm) {
   pm[0] = CRGB::Black;
 }
 displayDigit(min_1, temp_minutes / 10, color, Mode);
 displayDigit(min_2, temp_minutes % 10, color, Mode);
 // if (lastweekday != weekday) {
 //   // Serial.print("last week:");
 //   // Serial.print(lastweekday);
 //   // Serial.print(" ");
 //   // Serial.print("week:");
 //   // Serial.println(weekday);
 //   lastweekday = weekday;
 //   highlightDay(weekday_bg_color, weekday_selected_color, weekday);
 // }
}
void displayHumidity(uint8_t color, enum color_mode Mode) {
 int temp_humidity = (int)humidity;
 if ((temp_humidity / 100) != 0) {
   displayDigit(hour_1, temp_humidity / 100, color, Mode);
   displayDigit(hour_2, (temp_humidity / 10) % 10, color, Mode);
   displayDigit(min_1, temp_humidity % 10, color, Mode);
 } else {
   displayDigit(hour_1, (temp_humidity / 10) % 10, color, Mode);
   displayDigit(hour_2, temp_humidity % 10, color, Mode);
 }
 displayDigit(min_2, 10, color, Mode);
}
void displayTemperature(uint8_t color, enum color_mode Mode) {
 int temp_temperature = (int)temperature;
 if ((temp_temperature / 100) != 0) {
   displayDigit(hour_1, temp_temperature / 100, color, Mode);
   displayDigit(hour_2, (temp_temperature / 10) % 10, color, Mode);
   displayDigit(min_1, temp_temperature % 10, color, Mode);
 } else {
   displayDigit(hour_1, (temp_temperature / 10) % 10, color, Mode);
   displayDigit(hour_2, temp_temperature % 10, color, Mode);
 }
 if (!is_temp_in_f) {
   displayDigit(min_2, 12, color, Mode);
 } else {
   displayDigit(min_2, 11, color, Mode);
 }
}
/**
* Function to highlight a specific day of the week
* 
* @param bgColor - Background color for all LEDs
* @param hlColor - Highlight color for the specified day
* @param dayOfWeek - Day to highlight (0-6, where 0=Sunday, 6=Saturday)
*/
void highlightDay(CRGB bgColor, CRGB hlColor, int dayOfWeek) {
 // Validate day input
 if (dayOfWeek < 0 || dayOfWeek > 6) {
   Serial.print("Invalid day: ");
   Serial.println(dayOfWeek);
   Serial.println("Please use 0-6 (0=Sunday, 6=Saturday)");
   return;
 }
 // Set background color for all days
 for (int i = 0; i < 2; i++) {
   sun[i] = bgColor;
   mon[i] = bgColor;
   tue[i] = bgColor;
   wed[i] = bgColor;
   thu[i] = bgColor;
   fri[i] = bgColor;
   sat[i] = bgColor;
 }
 // Highlight the current day
 switch (dayOfWeek) {
   case 0:  // Sunday
     sun[0] = hlColor;
     sun[1] = hlColor;
     break;
   case 1:  // Monday
     mon[0] = hlColor;
     mon[1] = hlColor;
     break;
   case 2:  // Tuesday
     tue[0] = hlColor;
     tue[1] = hlColor;
     break;
   case 3:  // Wednesday
     wed[0] = hlColor;
     wed[1] = hlColor;
     break;
   case 4:  // Thursday
     thu[0] = hlColor;
     thu[1] = hlColor;
     break;
   case 5:  // Friday
     fri[0] = hlColor;
     fri[1] = hlColor;
     break;
   case 6:  // Saturday
     sat[0] = hlColor;
     sat[1] = hlColor;
     break;
 }
}
void clearAllSegments_NoWeek() {
 fill_solid(leds, NUM_LEDS, CRGB::Black);
 highlightDay(weekday_bg_color, weekday_selected_color, weekday);
}
void clearAllSegments() {
 fill_solid(leds, NUM_LEDS, CRGB::Black);
}
// ===================== get color =====================
CRGB getColorFromEnum(enum color_of_time color) {
 switch (color) {
   case RED: return CRGB::Red;
   case GREEN: return CRGB::Green;
   case BLUE: return CRGB::Blue;
   case WHITE: return CRGB::White;
   case BLACK: return CRGB::Black;
   case YELLOW: return CRGB::Yellow;
   case CYAN: return CRGB::Cyan;
   case MAGENTA: return CRGB::Magenta;
   case ORANGE: return CRGB::Orange;
   case PURPLE: return CRGB::Purple;
   case PINK: return CRGB::Pink;
   case TEAL: return CRGB::Teal;
   case LIME: return CRGB::Lime;
   case AMBER: return CRGB(255, 194, 0);        // Custom amber
   case GOLD: return CRGB(255, 215, 0);         // Custom gold
   case CORAL: return CRGB(255, 127, 80);       // Custom coral
   case LAVENDER: return CRGB(230, 230, 250);   // Custom lavender
   case SKYBLUE: return CRGB(135, 206, 235);    // Custom sky blue
   case FORESTGREEN: return CRGB(34, 139, 34);  // Custom forest green
   case MAROON: return CRGB(128, 0, 0);
   case OLIVE: return CRGB(128, 128, 0);
   case NAVY: return CRGB(0, 0, 128);
   case INDIGO: return CRGB(75, 0, 130);
   default: return CRGB::White;
 }
}
// ===================== Update Time =====================
/**
* @brief Get either 12-hour format hour or minute from 24-hour time input
* @param hour24 Hour in 24-hour format (0-23)
* @param minute Minute (0-59)
* @param mode Return mode: 0 for 12-hour hour, 1 for minute
* @return Requested time component, or -1 for invalid mode
*/
int getTimeComponent(int hour24, int minute, int mode) {
 if (mode == 0) {  // Return 12-hour format hour
   if (hour24 == 0) {
     return 12;  // Midnight case
   } else if (hour24 > 12) {
     return hour24 - 12;  // Afternoon case
   } else {
     return hour24;  // Morning case
   }
 } else if (mode == 1) {  // Return minute
   return minute;
 }
 // Invalid mode
 return -1;
}
void init_time_pull(const char *zoneName) {
 // Get POSIX timezone string from name
 tzString = getPosixString(zoneName);
 if (tzString == "") {
   tzString = "IST-5:30";  // Default fallback (Asia/Kolkata)
   Serial.println(" Timezone not found, using default");
 }
 Serial.print("Timezone POSIX string: ");
 Serial.println(tzString);
 // Configure timezone and NTP
 configTzTime(tzString.c_str(), "pool.ntp.org", "time.nist.gov");
}
void setTimezone(const char *zoneName) {
 // Get POSIX timezone string from name
 tzString = getPosixString(zoneName);
 if (tzString == "") {
   tzString = "IST-5:30";  // Default fallback (Asia/Kolkata)
   Serial.println(" Timezone not found, using default");
 }
 Serial.print("Timezone POSIX string: ");
 Serial.println(tzString);
 setenv("TZ", tzString.c_str(), 1);
 // Apply the new timezone setting[citation:3][citation:10]
 tzset();
}
void updateTime() {
 // Get current date and time
 DateTime now = rtc.now();
 hours = now.hour();
 minutes = now.minute();
 seconds = now.second();
 is_pm = now.isPM();
 date = now.day();
 month = now.month();
 year = now.year();
 // Serial.print("DateTime: ");
 // Serial.print(year);
 // Serial.print('-');
 // Serial.print(month);
 // Serial.print('-');
 // Serial.println(date);
 weekday = now.dayOfTheWeek();
 // Serial.print("Update week:");
 // Serial.println(weekday);
}
void updateTimeFromNTP() {
 // Serial.println(" ENTRY IN TIME SYNCHRONIZE");
 // Get time from system (already adjusted for timezone)
 time_t now = time(nullptr);
 //  NTP not ready yet (non-blocking check)
 if (now < 100000) {
   // Serial.println("NTP not ready yet");
   return;
 }
 struct tm timeinfo;
 localtime_r(&now, &timeinfo);
 uint8_t currentHour = timeinfo.tm_hour;
 uint8_t currentMinute = timeinfo.tm_min;
 uint8_t currentSecond = timeinfo.tm_sec;
 uint8_t currentDate = timeinfo.tm_mday;
 uint8_t currentMonth = timeinfo.tm_mon + 1;  // tm_mon is 0-11
 int currentYear = timeinfo.tm_year + 1900;   // tm_year is years since 1900
 if (date != currentDate || month != currentMonth || year != currentYear || hours != currentHour || minutes != currentMinute || seconds != currentSecond) {
   rtc.adjust(DateTime(currentYear, currentMonth, currentDate, currentHour, currentMinute, currentSecond));
 }
 if (!timeSynced) {
   timeSynced = true;
   Serial.println("\n══════════════════════════════════════");
   Serial.println("    TIME SYNCHRONIZED WITH RTC!");
   Serial.println(" ══════════════════════════════════════\n");
 }
}
// ===================== Wifi Handle =====================
void startAPMode() {
 if (apModeActive) return;
 Serial.println("\n Starting AP Mode...");
 wifiConnected = false;
 timeSynced = false;
 server_stop();  // stop mDNS & web server
 delay(100);
 WiFi.disconnect(true, true);  // drop STA + erase runtime state
 delay(200);
 WiFi.mode(WIFI_OFF);  //  THIS IS THE KEY LINE
 delay(300);           // allow netif cleanup
 // ---- NOW SAFE TO START AP ----
 WiFi.mode(WIFI_AP);
 // delay(5000);
 // // update_hostname();
 String apSSID = String(mdnsHostname);
 bool ok = false;
 if (AP_PASSWORD && strlen(AP_PASSWORD) >= 8) {
   // Protected AP
   ok = WiFi.softAP(apSSID.c_str(), AP_PASSWORD, 6);
 } else {
   // OPEN AP — IMPORTANT: empty string, NOT nullptr
   ok = WiFi.softAP(apSSID.c_str(), "", 6);
 }
 if (!ok) {
   Serial.println(" Failed to start AP");
   return;
 }
 server_start();  // start mDNS & web server
 delay(100);
 apModeActive = true;
 IPAddress ip = WiFi.softAPIP();
 Serial.println(" AP STARTED");
 Serial.print("  SSID: ");
 Serial.println(apSSID);
 Serial.print("  IP: ");
 Serial.println(ip);
 dnsServer.start(53, "*", ip);
 WiFi.setSleep(false);
}
void stopAPMode() {
 if (!apModeActive) return;
 server_stop();  // stop mDNS & web server
 delay(100);
 Serial.println(" Stopping AP Mode...");
 WiFi.softAPdisconnect(true);
 apModeActive = false;
 delay(500);
}
void WifiHandle() {
 if (!wifiConnected && WiFi.status() == WL_CONNECTED) {
   server_start();
   wifiConnected = true;
   Serial.println("\n✓ WiFi CONNECTED!");
   Serial.print("IP Address: ");
   Serial.println(WiFi.localIP());
   init_time_pull(timezone.c_str());
 }
 // if (!wifiConnected && WiFi.status() != WL_CONNECTED) {
 // }
 if (wifiConnected && WiFi.status() != WL_CONNECTED) {
   wifiConnected = false;
   timeSynced = false;
   Serial.println("\n✗ WiFi DISCONNECTED! Using local time...");
   server_stop();
 }
 // Handle NTP sync
 if (wifiConnected && !timeSynced) {
   updateTimeFromNTP();
 }
 // Update from NTP periodically if connected
 if (wifiConnected && timeSynced) {
   if (currentMillis - lastUpdateTime >= 2000) {  // Try every 2 seconds
     lastUpdateTime = currentMillis;
     updateTimeFromNTP();
   }
 }
}
// ===================== Print Debug =====================
void PrintDebug() {
 if (currentMillis - lastPrintTime >= 1000 * 60 * 1) {  //print in 1 minute
   lastPrintTime = currentMillis;
   Serial.print("DateTime: ");
   Serial.print(year);
   Serial.print('-');
   Serial.print(month);
   Serial.print('-');
   Serial.print(date);
   Serial.print(" ");
   Serial.print(daysOfWeek[weekday]);
   Serial.print(" ");
   Serial.print(hours);
   Serial.print(':');
   Serial.print(minutes);
   Serial.print(':');
   Serial.print(seconds);
   Serial.print(" ");
   if (!enable_24_hour_mode) {
     Serial.println(is_pm ? "PM" : "AM");  // Prints AM or PM
   } else {
     Serial.println();
   }
   Serial.print(F("Humidity: "));
   Serial.print(humidity);
   Serial.print(F("%  Temperature: "));
   Serial.print(temperature);
   Serial.println(F(is_temp_in_f ? "°F" : "°C "));
   Serial.print(F("LDR Raw: "));
   Serial.print(rawValue);
   Serial.print(F("  Bright level: "));
   Serial.println(smoothedLightLevel);
 }
}
/* ================== HELPERS ================== */
void update_hostname() {
 uint8_t mac[6];
 WiFi.macAddress(mac);
 snprintf(mdnsHostname, sizeof(mdnsHostname),
          "Clock-%02X%02X%02X", mac[3], mac[4], mac[5]);
}
String getMacAddress() {
 String mac = WiFi.macAddress();
 mac.replace(":", "");
 mac.toLowerCase();
 return mac;
}
/* ================== LOAD PREFS ================== */
void loadWiFiCredentials() {
 wifiPrefs.begin("wifi", true);  // read-only
 wifiSSID = wifiPrefs.getString("ssid", "");
 wifiPassword = wifiPrefs.getString("pass", "");
 wifiPrefs.end();
}
void loadPreferences() {
 prefs.begin("clock", false);
 // Clock settings
 brightness = prefs.getInt("brightness", 80);
 autoBrightness = prefs.getBool("autoBrightness", false);
 is_pir_on = prefs.getBool("powerSaving", false);
 timezone = prefs.getString("timezone", "Asia/Kolkata");
 timeMode = prefs.getString("timeMode", "24");
 displayOnly = prefs.getString("displayOnly", "all");
 display_cycle_time = prefs.getInt("rotateTime", 5);
 time_out_for_led_off = prefs.getInt("displayOnTime", 1);
 is_theft_on = prefs.getBool("theftDetection", false);
 theftSoundTimeout = prefs.getInt("theftSoundTimeout", 10);
 theftTimingEnabled = prefs.getBool("theftTimeEn", false);
 // Theft timing
 theftStartTime.hour = prefs.getUChar("theftStartHour", 23);
 theftStartTime.minute = prefs.getUChar("theftStartMinute", 0);
 theftEndTime.hour = prefs.getUChar("theftEndHour", 6);
 theftEndTime.minute = prefs.getUChar("theftEndMinute", 0);
 // Color settings
 colorMode = prefs.getString("colorMode", "solid");
 solidColor = prefs.getString("solidColor", "red");
 effect = prefs.getString("effect", "rainbow");
 // Theme
 lightTheme = prefs.getBool("lightTheme", false);
 // Alarms
 for (int i = 0; i < ALARM_COUNT; i++) {
   String k = "a" + String(i);
   alarms[i].enabled = prefs.getBool((k + "_en").c_str(), false);
   alarms[i].hour = alarms[i].back_up_h = prefs.getUChar((k + "_h").c_str(), 7);
   alarms[i].minute = alarms[i].back_up_m = prefs.getUChar((k + "_m").c_str(), 0);
 }
 prefs.end();
 setTimezone(timezone.c_str());                                       //set TimeZone
 timeModeHandle(timeMode);                                            //24/12 hour
 displayOnlyHandle(displayOnly);                                      //display switch
 ColorUpdate(colorMode.c_str(), solidColor.c_str(), effect.c_str());  //load color

 // Log to serial
 Serial.println(" CLOCK SETTINGS SAVED");
 Serial.printf("  Brightness: %d\n", brightness);
 Serial.printf("  AutoBrightness: %s\n", autoBrightness ? "ON" : "OFF");
 Serial.printf("  PowerSaving: %s\n", is_pir_on ? "ON" : "OFF");
 Serial.printf("  Timezone: %s\n", timezone.c_str());
 Serial.printf("  Time Mode: %s hour\n", timeMode.c_str());
 Serial.printf("  Display Only: %s\n", displayOnly.c_str());
 Serial.printf("  Rotate Time: %d seconds\n", display_cycle_time);
 Serial.printf("  Display ON Time: %d minutes\n", time_out_for_led_off);
 Serial.printf("  Theft Detection: %s\n", is_theft_on ? "ON" : "OFF");
 Serial.printf("  Theft Sound Timeout: %d seconds\n", theftSoundTimeout);
 Serial.printf("  Theft Timing Enabled: %s\n", theftTimingEnabled ? "YES" : "NO");
 if (theftTimingEnabled) {
   Serial.printf("  Theft Start Time: %02d:%02d\n", theftStartTime.hour, theftStartTime.minute);
   Serial.printf("  Theft End Time: %02d:%02d\n", theftEndTime.hour, theftEndTime.minute);
 }
}
/* ================== API HANDLERS ================== */
void handleRoot() {
 server.send_P(200, "text/html", RGB_CLOCK_HTML);
}
void handleWin() {
 // WLED discovery endpoint
 String xml = "<?xml version=\"1.0\"?><vs>";
 xml += "<ac>128</ac>";
 xml += "<cl>255</cl><cl>128</cl><cl>0</cl>";
 xml += "<ds>" + String(mdnsHostname) + "</ds>";
 xml += "</vs>";
 server.send(200, "text/xml", xml);
}
/* ---- GET ALL SETTINGS ---- */
void handleGet() {
 StaticJsonDocument<2048> doc;
 // Clock settings
 doc["brightness"] = brightness;
 doc["autoBrightness"] = autoBrightness;
 doc["powerSaving"] = is_pir_on;
 doc["timezone"] = timezone;
 doc["timeMode"] = timeMode;
 doc["displayOnly"] = displayOnly;
 doc["rotateTime"] = display_cycle_time;
 doc["displayOnTime"] = time_out_for_led_off;
 doc["theftDetection"] = is_theft_on;
 doc["theftSoundTimeout"] = theftSoundTimeout;
 doc["theftTimingEnabled"] = theftTimingEnabled;
 // Theft timing
 JsonObject theftStart = doc.createNestedObject("theftStartTime");
 theftStart["hour"] = theftStartTime.hour;
 theftStart["minute"] = theftStartTime.minute;
 JsonObject theftEnd = doc.createNestedObject("theftEndTime");
 theftEnd["hour"] = theftEndTime.hour;
 theftEnd["minute"] = theftEndTime.minute;
 // Color settings
 doc["colorMode"] = colorMode;
 doc["solidColor"] = solidColor;
 doc["effect"] = effect;
 // Theme
 doc["lightTheme"] = lightTheme;
 // Alarms
 JsonArray arr = doc.createNestedArray("alarms");
 for (int i = 0; i < ALARM_COUNT; i++) {
   JsonObject a = arr.createNestedObject();
   a["enabled"] = alarms[i].enabled;
   a["hour"] = alarms[i].back_up_h;
   a["minute"] = alarms[i].back_up_m;
 }
 String out;
 serializeJson(doc, out);
 server.send(200, "application/json", out);
}
/* ---- OTA server ---- */
#define __FILENAME__ (strrchr(__FILE__, '\\\\') ? strrchr(__FILE__, '\\\\') + 1 : __FILE__)
#define FW_VERSION __FILENAME__
const char *firmwareVersion() {
 static char name[64];  // adjust size if needed
 static bool init = false;
 if (!init) {
   strncpy(name, __FILENAME__, sizeof(name) - 1);
   name[sizeof(name) - 1] = '\0';
   char *dot = strrchr(name, '.');
   if (dot) *dot = '\0';  //  remove extension
   init = true;
 }
 return name;
}
void handleOtaInfo() {
 StaticJsonDocument<64> doc;
 doc["version"] = firmwareVersion();
 Serial.print("FILE NAME:");
 Serial.println(firmwareVersion());
 String out;
 serializeJson(doc, out);
 server.send(200, "application/json", out);
}
void handleOtaUpload() {
 HTTPUpload &upload = server.upload();
 if (upload.status == UPLOAD_FILE_START) {
   Serial.printf("OTA Start: %s\n", upload.filename.c_str());
   if (!Update.begin(UPDATE_SIZE_UNKNOWN)) {
     Update.printError(Serial);
   }
 } else if (upload.status == UPLOAD_FILE_WRITE) {
   if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
     Update.printError(Serial);
   }
 } else if (upload.status == UPLOAD_FILE_END) {
   if (Update.end(true)) {
     Serial.println("OTA Success");
   } else {
     Update.printError(Serial);
   }
 }
}
void handleOtaFinish() {
 server.send(200, "text/plain", "OK");
 delay(1000);
 ESP.restart();
}

/* ---- Wifi On server ---- */
void handleSaveWifi() {
 StaticJsonDocument<256> doc;
 deserializeJson(doc, server.arg("plain"));
 stopAPMode();
 wifiSSID = doc["ssid"].as<String>();
 wifiPassword = doc["password"].as<String>();
 // Save to preferences
 wifiPrefs.begin("wifi", false);
 wifiPrefs.putString("ssid", wifiSSID);
 wifiPrefs.putString("pass", wifiPassword);
 wifiPrefs.end();
 Serial.println("WIFI SETTINGS SAVED");
 Serial.printf("  SSID: %s\n", wifiSSID.c_str());
 server.send(200, "text/plain", "WiFi saved");
 // Reconnect cleanly
 WiFi.disconnect(true);
 delay(200);
 WiFi.mode(WIFI_STA);
 delay(100);
 WiFi.begin(wifiSSID.c_str(), wifiPassword.c_str());
 wifiConnected = false;
 timeSynced = false;
}
 
void handleWifiStatus() {
 StaticJsonDocument<256> doc;
 doc["connected"] = (WiFi.status() == WL_CONNECTED);
 doc["ssid"] = wifiSSID;
 doc["ip"] = WiFi.isConnected() ? WiFi.localIP().toString() : "";
 doc["rssi"] = WiFi.isConnected() ? WiFi.RSSI() : 0;
 //  optional – only because YOU asked for it
 doc["password"] = wifiPassword;
 String out;
 serializeJson(doc, out);
 server.send(200, "application/json", out);
}

/* ---- CLOCK SAVE ---- */
void handleSaveClock() {
 StaticJsonDocument<512> doc;
 deserializeJson(doc, server.arg("plain"));
 if (digitalRead(BUSY_AUDIO)) {
   stop_audio();
 }
 if ((is_pir_on || is_theft_on) && turn_on_relay) {
   turn_on_relay = 0;
   TurnOffDisplay();
 }
 // Save to preferences
 prefs.begin("clock", false);
 // Basic clock settings
 brightness = doc["brightness"];
 autoBrightness = doc["autoBrightness"];
 is_pir_on = doc["powerSaving"];
 timezone = doc["timezone"].as<String>();
 timeMode = doc["timeMode"].as<String>();
 displayOnly = doc["displayOnly"].as<String>();
 display_cycle_time = doc["rotateTime"];
 time_out_for_led_off = doc["displayOnTime"];
 is_theft_on = doc["theftDetection"];
 theftSoundTimeout = doc["theftSoundTimeout"];
 theftTimingEnabled = doc["theftTimingEnabled"];
 prefs.putInt("brightness", brightness);
 prefs.putBool("autoBrightness", autoBrightness);
 prefs.putBool("powerSaving", is_pir_on);
 prefs.putString("timezone", timezone);
 prefs.putString("timeMode", timeMode);
 prefs.putString("displayOnly", displayOnly);
 prefs.putInt("rotateTime", display_cycle_time);
 prefs.putInt("displayOnTime", time_out_for_led_off);
 prefs.putBool("theftDetection", is_theft_on);
 prefs.putInt("theftSoundTimeout", theftSoundTimeout);
 prefs.putBool("theftTimeEn", theftTimingEnabled);
 if (doc.containsKey("theftStartTime")) {
   theftStartTime.hour = doc["theftStartTime"]["hour"];
   theftStartTime.minute = doc["theftStartTime"]["minute"];
   prefs.putUChar("theftStartHour", theftStartTime.hour);
   prefs.putUChar("theftStartMinute", theftStartTime.minute);
 }
 if (doc.containsKey("theftEndTime")) {
   theftEndTime.hour = doc["theftEndTime"]["hour"];
   theftEndTime.minute = doc["theftEndTime"]["minute"];
   prefs.putUChar("theftEndHour", theftEndTime.hour);
   prefs.putUChar("theftEndMinute", theftEndTime.minute);
 }
 prefs.end();
 setTimezone(timezone.c_str());   //set TimeZone
 timeModeHandle(timeMode);        //24/12 hour
 displayOnlyHandle(displayOnly);  //display switch

 // Log to serial
 Serial.println(" CLOCK SETTINGS SAVED");
 Serial.printf("  Brightness: %d\n", brightness);
 Serial.printf("  AutoBrightness: %s\n", autoBrightness ? "ON" : "OFF");
 Serial.printf("  PowerSaving: %s\n", is_pir_on ? "ON" : "OFF");
 Serial.printf("  Timezone: %s\n", timezone.c_str());
 Serial.printf("  Time Mode: %s hour\n", timeMode.c_str());
 Serial.printf("  Display Only: %s\n", displayOnly.c_str());
 Serial.printf("  Rotate Time: %d seconds\n", display_cycle_time);
 Serial.printf("  Display ON Time: %d minutes\n", time_out_for_led_off);
 Serial.printf("  Theft Detection: %s\n", is_theft_on ? "ON" : "OFF");
 Serial.printf("  Theft Sound Timeout: %d seconds\n", theftSoundTimeout);
 Serial.printf("  Theft Timing Enabled: %s\n", theftTimingEnabled ? "YES" : "NO");
 if (theftTimingEnabled) {
   Serial.printf("  Theft Start Time: %02d:%02d\n", theftStartTime.hour, theftStartTime.minute);
   Serial.printf("  Theft End Time: %02d:%02d\n", theftEndTime.hour, theftEndTime.minute);
 }
 server.send(200, "text/plain", "Clock saved");
}
/* ---- COLORS SAVE ---- */
void handleSaveColors() {
 Serial.println(" /api/colors/save HIT");
 StaticJsonDocument<128> doc;
 deserializeJson(doc, server.arg("plain"));
 prefs.begin("clock", false);
 colorMode = doc["mode"].as<String>();
 solidColor = doc["color"].as<String>();
 effect = doc["effect"].as<String>();
 prefs.putString("colorMode", colorMode);
 prefs.putString("solidColor", solidColor);
 prefs.putString("effect", effect);
 prefs.end();
 ColorUpdate(colorMode.c_str(), solidColor.c_str(), effect.c_str());
 Serial.println(" COLOR SETTINGS SAVED");
 Serial.printf("  Mode: %s\n", colorMode.c_str());
 Serial.printf("  Solid Color: %s\n", solidColor.c_str());
 Serial.printf("  Effect: %s\n", effect.c_str());
 server.send(200, "text/plain", "Colors saved");
}
/* ---- COLORS PREVIEW ---- */
void handlePreviewColors() {
 Serial.println(" Color Preview:");
 // Serial.println(server.arg("plain"));
 // server.send(200, "text/plain", "Preview OK");
 handleSaveColors();
}
/* ---- ALARMS SAVE ---- */
void handleSaveAlarms() {
 Serial.println(" /api/alarms/save HIT");
 StaticJsonDocument<512> doc;
 deserializeJson(doc, server.arg("plain"));
 JsonArray arr = doc.as<JsonArray>();
 if (digitalRead(BUSY_AUDIO)) {
   stop_audio();
 }
 prefs.begin("clock", false);
 for (int i = 0; i < ALARM_COUNT && i < arr.size(); i++) {
   alarms[i].enabled = arr[i]["enabled"];
   alarms[i].hour = alarms[i].back_up_h = arr[i]["hour"];
   alarms[i].minute = alarms[i].back_up_m = arr[i]["minute"];
   String k = "a" + String(i);
   prefs.putBool((k + "_en").c_str(), alarms[i].enabled);
   prefs.putUChar((k + "_h").c_str(), alarms[i].hour);
   prefs.putUChar((k + "_m").c_str(), alarms[i].minute);
   Serial.printf(
     "  Alarm %d → %s at %02d:%02d\n",
     i + 1,
     alarms[i].enabled ? "ENABLED" : "DISABLED",
     alarms[i].hour,
     alarms[i].minute);
 }
 prefs.end();
 // Serial.println(" Alarms saved:");
 // serializeJsonPretty(arr, Serial);
 server.send(200, "text/plain", "Alarms saved");
}
/* ---- PLAY ALARM ---- */
void handlePlayAlarm() {
 StaticJsonDocument<32> doc;
 deserializeJson(doc, server.arg("plain"));
 int PlayAudioNumber = doc["alarm"];
 Serial.printf(" Playing alarm %d\n", PlayAudioNumber);
 if (digitalRead(BUSY_AUDIO)) {
   stop_audio();
 }
 mp3.setLoopMode(MP3_LOOP_ONE_STOP);
 mp3.playFileByIndexNumber(alarms[PlayAudioNumber - 1].sound_num);
 server.send(200, "text/plain", "Playing alarm");
}
/* ---- THEME SAVE ---- */
void handleTheme() {
 StaticJsonDocument<32> doc;
 deserializeJson(doc, server.arg("plain"));
 lightTheme = doc["light"];
 prefs.putBool("lightTheme", lightTheme);
 Serial.print("Theme:");
 Serial.println(lightTheme);
 server.send(200, "text/plain", "Theme saved");
}
void server_start() {
 if (serverRunning) return;
 server.on("/win", handleWin);  // WLED discovery endpoint
 server.on("/", handleRoot);
 server.on("/api/get", HTTP_GET, handleGet);
 server.on("/api/clock/save", HTTP_POST, handleSaveClock);
 server.on("/api/colors/save", HTTP_POST, handleSaveColors);
 server.on("/api/colors/preview", HTTP_POST, handlePreviewColors);
 server.on("/api/alarms/save", HTTP_POST, handleSaveAlarms);
 server.on("/api/alarm/play", HTTP_POST, handlePlayAlarm);
 server.on("/api/theme", HTTP_POST, handleTheme);
 server.on("/api/wifi/status", HTTP_GET, handleWifiStatus);
 server.on("/api/wifi/save", HTTP_POST, handleSaveWifi);
 server.on("/api/ota/info", HTTP_GET, handleOtaInfo);
 server.on("/api/ota/update", HTTP_POST, handleOtaFinish, handleOtaUpload);

 // ---- Captive Portal Detection URLs ----
 server.on("/generate_204", HTTP_GET, []() {
   server.send(200, "text/plain", "");
 });
 server.on("/hotspot-detect.html", HTTP_GET, []() {
   server.send_P(200, "text/html", RGB_CLOCK_HTML);
 });
 server.on("/ncsi.txt", HTTP_GET, []() {
   server.send(200, "text/plain", "Microsoft NCSI");
 });
 // ---- Catch ALL other URLs ----
 server.onNotFound([]() {
   server.sendHeader("Location", "http://192.168.4.1", true);
   server.send(302, "text/plain", "");
 });
 server.begin();
 MDNS_start();
 Serial.println("Ready");
 Serial.println("==================================");
 Serial.printf("Device name: %s\n", mdnsHostname);
 Serial.printf("Access via: http://%s.local\n", mdnsHostname);
 // Serial.printf("Or IP: http://%s\n", WiFi.localIP().toString().c_str());
 Serial.println("Ready! Scan with WLED app.");
 Serial.println("==================================");
 serverRunning = true;
}
void server_stop() {
 if (!serverRunning) return;
 server.stop();
 MDNS_stop();
 serverRunning = false;
 Serial.println(" Server stopped");
}
void MDNS_start() {
 if (mdnsRunning) return;
 // update_hostname();
 if (MDNS.begin(mdnsHostname)) {
   String mac = getMacAddress();
   MDNS.addService("http", "tcp", 80);
   MDNS.addService("wled", "tcp", 80);
   MDNS.addServiceTxt("wled", "tcp", "mac", mac.c_str());
   mdnsRunning = true;
 }
}
void MDNS_stop() {
 if (!mdnsRunning) return;
 MDNS.end();
 mdnsRunning = false;
 Serial.println(" mDNS stopped");
}
/*============================= Web Page to ESP ====================================*/
void timeModeHandle(const String &_timeMode) {
 enable_24_hour_mode = (_timeMode == "24");
}
void displayOnlyHandle(const String &displayOnly_) {
 if (displayOnly_ == "time") {
   Display_Value = 0;
 }
 if (displayOnly_ == "humidity") {
   Display_Value = 1;
 }
 if (displayOnly_ == "temperature") {
   Display_Value = 2;
 }
 clearAllSegments_NoWeek();
}
void ColorUpdate(const String &ColorMode_, const String &Color_, const String &Effect_) {
 if (ColorMode_ == "solid") {
   Color_Mode = SOLID;
   current_color = StringToColor(Color_.c_str());
 }
 if (ColorMode_ == "effect") {
   Color_Mode = EFFECT;
   current_effect = StringToEffect(Effect_.c_str());
 }
}
color_of_time StringToColor(const String &solidColor_) {
 String c = solidColor_;
 c.toLowerCase();  // make comparison case-insensitive
 if (c == "red") return RED;
 if (c == "green") return GREEN;
 if (c == "blue") return BLUE;
 if (c == "white") return WHITE;
 if (c == "black") return BLACK;
 if (c == "yellow") return YELLOW;
 if (c == "cyan") return CYAN;
 if (c == "magenta") return MAGENTA;
 if (c == "orange") return ORANGE;
 if (c == "purple") return PURPLE;
 if (c == "pink") return PINK;
 if (c == "teal") return TEAL;
 if (c == "lime") return LIME;
 if (c == "amber") return AMBER;
 if (c == "gold") return GOLD;
 if (c == "coral") return CORAL;
 if (c == "lavender") return LAVENDER;
 if (c == "skyblue") return SKYBLUE;
 if (c == "forestgreen") return FORESTGREEN;
 if (c == "maroon") return MAROON;
 if (c == "olive") return OLIVE;
 if (c == "navy") return NAVY;
 if (c == "indigo") return INDIGO;
 // fallback if no match
 return current_color;  // or choose a fixed default like BLACK
}
effect_of_time StringToEffect(const String &effect_) {
 String c = effect_;
 c.toLowerCase();  // make comparison case-insensitive
 if (c == "rainbow") return RAINBOW_1;
 return current_effect;
}
Have any question related to this Article?

Add New Comment

Login to Comment Sign in with Google Log in with Facebook Sign in with GitHub