// Auto Bell System with Modular UI and SD Card JSON Schedule
#include <Wire.h>
#include <RTClib.h>
#include <LiquidCrystal_I2C.h>
#include <SD.h>
#include <ArduinoJson.h>
#include <Bounce2.h>
#include <algorithm>
const int bellPin = 5;
const int chipSelect = 15;
const int maxAlarms = 20;
RTC_DS3231 rtc;
LiquidCrystal_I2C lcd(0x27, 16, 2);
// Button Constants
const int btnUp = 32, btnDown = 33, btnLeft = 25, btnRight = 26, btnHome = 27;
Bounce debouncers[5];
const int btnPins[5] = {btnUp, btnDown, btnLeft, btnRight, btnHome};
// Schedule Data
String scheduleNames[] = {"schedule_normal_day", "schedule_half_day"};
int currentScheduleIndex = 0;
int h[maxAlarms], m[maxAlarms], numAlarms = 0;
int scheduleViewIndex = 0;
// Timing
unsigned long lastScreenChange = 0, lastInteractionTime = 0;
const unsigned long screenCycleInterval = 20000, defaultReturnTime = 30000;
int infoCycle = 0;
int lastRungHour = -1, lastRungMinute = -1;
bool bellRungThisMinute = false;
// Menu State
enum MenuState { MAIN_MENU, VIEW_TIME, VIEW_NEXT_BELL, SELECT_SCHEDULE, VIEW_SCHEDULE, DEBUG_MENU, SET_TIME };
MenuState menuState = MAIN_MENU;
int menuCursor = 0;
// Menu Options
const String menuOptions[] = {"View Time", "Next Bell", "Select Schedule", "View Schedule", "Debug Menu", "Set Time"};
const int numMenuOptions = sizeof(menuOptions) / sizeof(menuOptions[0]);
void setup() {
Serial.begin(9600);
lcd.init(); lcd.backlight();
for (int i = 0; i < 5; ++i) {
debouncers[i].attach(btnPins[i], INPUT_PULLUP);
debouncers[i].interval(25);
}
pinMode(bellPin, OUTPUT); digitalWrite(bellPin, LOW);
if (!rtc.begin()) { lcd.setCursor(0, 0); lcd.print("RTC ERROR"); while (1); }
if (!SD.begin(chipSelect)) { lcd.setCursor(0, 0); lcd.print("SD INIT FAIL"); while (1); }
loadSchedule(scheduleNames[currentScheduleIndex]);
lcd.clear(); lcd.setCursor(0, 0); lcd.print("Auto Bell Ready"); delay(1000); lcd.clear();
}
void loop() {
for (int i = 0; i < 5; ++i) debouncers[i].update();
handleButtons();
updateScreen();
checkSerialInput();
checkBellTime();
if (millis() - lastInteractionTime > defaultReturnTime && menuState != VIEW_NEXT_BELL) menuState = VIEW_NEXT_BELL;
}
void handleButtons() {
bool pressed = false;
if (debouncers[4].fell()) { menuState = MAIN_MENU; pressed = true; }
switch (menuState) {
case MAIN_MENU:
if (debouncers[0].fell()) { menuCursor = (menuCursor - 1 + numMenuOptions) % numMenuOptions; pressed = true; }
if (debouncers[1].fell()) { menuCursor = (menuCursor + 1) % numMenuOptions; pressed = true; }
if (debouncers[3].fell()) {
menuState = (MenuState)(menuCursor + 1);
if (menuState == VIEW_SCHEDULE) scheduleViewIndex = 0;
pressed = true;
}
break;
case SELECT_SCHEDULE:
if (debouncers[0].fell()) { currentScheduleIndex = (currentScheduleIndex + 1) % (sizeof(scheduleNames)/sizeof(scheduleNames[0])); loadSchedule(scheduleNames[currentScheduleIndex]); scheduleViewIndex = 0; pressed = true; }
if (debouncers[1].fell()) { currentScheduleIndex = (currentScheduleIndex - 1 + (sizeof(scheduleNames)/sizeof(scheduleNames[0]))) % (sizeof(scheduleNames)/sizeof(scheduleNames[0])); loadSchedule(scheduleNames[currentScheduleIndex]); scheduleViewIndex = 0; pressed = true; }
if (debouncers[2].fell()) { menuState = MAIN_MENU; pressed = true; }
if (debouncers[3].fell()) { menuState = VIEW_SCHEDULE; scheduleViewIndex = 0; pressed = true; }
break;
case VIEW_SCHEDULE:
if (debouncers[0].fell() && scheduleViewIndex > 0) { scheduleViewIndex--; pressed = true; }
if (debouncers[1].fell() && scheduleViewIndex < numAlarms - 1) { scheduleViewIndex++; pressed = true; }
if (debouncers[2].fell()) { menuState = SELECT_SCHEDULE; pressed = true; }
break;
case DEBUG_MENU:
case VIEW_TIME:
case VIEW_NEXT_BELL:
case SET_TIME:
if (debouncers[2].fell()) { menuState = MAIN_MENU; pressed = true; }
break;
}
if (pressed) lastInteractionTime = millis();
}
void updateScreen() {
static unsigned long lastUpdate = 0;
if (millis() - lastUpdate < 200) return;
lastUpdate = millis();
lcd.clear();
switch (menuState) {
case MAIN_MENU:
lcd.setCursor(0, 0); lcd.print(">" + menuOptions[menuCursor]);
lcd.setCursor(0, 1); lcd.print(menuOptions[(menuCursor + 1) % numMenuOptions]);
break;
case VIEW_TIME: {
DateTime now = rtc.now();
lcd.setCursor(0, 0); lcd.print("Time: ");
lcd.print(formatDigits(now.hour()) + ":" + formatDigits(now.minute()));
break;
}
case VIEW_NEXT_BELL: {
DateTime now = rtc.now();
int currentMins = now.hour() * 60 + now.minute();
int nextIndex = -1;
for (int i = 0; i < numAlarms; i++) if (h[i]*60 + m[i] > currentMins) { nextIndex = i; break; }
lcd.setCursor(0, 0); lcd.print("Next Bell:");
if (nextIndex >= 0) {
if (millis() - lastScreenChange > screenCycleInterval) { lastScreenChange = millis(); infoCycle = (infoCycle + 1) % 3; }
lcd.setCursor(0, 1);
if (infoCycle == 0) lcd.print(formatDigits(h[nextIndex]) + ":" + formatDigits(m[nextIndex]));
else if (infoCycle == 1) { lcd.print("In " + String((h[nextIndex]*60 + m[nextIndex]) - currentMins) + " mins"); }
else lcd.print(scheduleNames[currentScheduleIndex]);
} else { lcd.setCursor(0, 1); lcd.print("No more today"); }
break;
}
case SELECT_SCHEDULE:
lcd.setCursor(0, 0); lcd.print("Schedule:");
lcd.setCursor(0, 1); lcd.print(scheduleNames[currentScheduleIndex]);
break;
case VIEW_SCHEDULE:
lcd.setCursor(0, 0); lcd.print(scheduleNames[currentScheduleIndex]);
lcd.setCursor(0, 1);
if (numAlarms > 0) lcd.print(String(scheduleViewIndex+1) + ". " + formatDigits(h[scheduleViewIndex]) + ":" + formatDigits(m[scheduleViewIndex]));
else lcd.print("No Bells");
break;
case DEBUG_MENU:
lcd.setCursor(0, 0); lcd.print("DEBUG: Serial");
lcd.setCursor(0, 1); lcd.print("settime|schedule");
break;
case SET_TIME:
lcd.setCursor(0, 0); lcd.print("Set via Serial");
break;
}
}
void loadSchedule(const String& scheduleName) {
File file = SD.open("bell.json");
if (!file) { lcd.setCursor(0, 0); lcd.print("No bell.json"); return; }
StaticJsonDocument<1024> doc;
DeserializationError err = deserializeJson(doc, file);
file.close();
if (err) { lcd.setCursor(0, 0); lcd.print("JSON error"); return; }
JsonArray schedule = doc[scheduleName];
numAlarms = std::min((int)schedule.size(), maxAlarms);
for (int i = 0; i < numAlarms; i++) { h[i] = schedule[i]["hr"]; m[i] = schedule[i]["min"]; }
}
void checkSerialInput() {
if (Serial.available()) {
String cmd = Serial.readStringUntil('\n');
cmd.trim();
if (cmd.startsWith("settime")) {
int hr = cmd.substring(8, 10).toInt();
int mn = cmd.substring(11, 13).toInt();
int sec = cmd.length() >= 16 ? cmd.substring(14, 16).toInt() : 0;
rtc.adjust(DateTime(2025, 1, 1, hr, mn, sec));
Serial.println("Time Set");
} else if (cmd == "schedule") {
Serial.println("Current Schedule:");
for (int i = 0; i < numAlarms; i++) Serial.println(formatDigits(h[i]) + ":" + formatDigits(m[i]));
} else {
Serial.println("Unknown Command");
}
}
}
void checkBellTime() {
DateTime now = rtc.now();
for (int i = 0; i < numAlarms; i++) {
if (now.hour() == h[i] && now.minute() == m[i]) {
if (!bellRungThisMinute) {
ringBell();
bellRungThisMinute = true;
lastRungHour = now.hour(); lastRungMinute = now.minute();
}
break;
}
}
if (now.minute() != lastRungMinute || now.hour() != lastRungHour) bellRungThisMinute = false;
}
void ringBell() {
digitalWrite(bellPin, HIGH); delay(2000); digitalWrite(bellPin, LOW);
}
String formatDigits(int num) {
return (num < 10 ? "0" : "") + String(num);
}