• Smart Bell V3.0: STM32 Industrial Controller with Review Mode, Auto Scheduling and Punjabi Audio

QC

Smart Bell V3.0: STM32 Industrial Controller with Review Mode, Auto Scheduling and Punjabi Audio

The STM32 Smart School Bell V3.0 is an industrial grade automation system developed in the Arduino IDE. It features a precision-engineered enclosure in Solid Edge 2025 for 3D printing and high-fidelity Punjabi voice alerts synthesized via ElevenLabs AI. With automated timetable generation and a robust hardware self-test diagnostics suite, it offers a professional, localized solution for modern schools and industrial facilities.

February 22, 2026

811 views

1 respect

Devices and components

DFPlayer Mini mp3 player

STM32F103C8T6 *optional

push buttons

20x4 LCD with I2C module

Micro SD 8 GB class 10

AT24C256 I2C Serial EEPROM Data Storage Module for Arduino

5V relay module

22pF capacitor

Active buzzer

CR2025 button cell battery socket holder

MAX7219 8x8 Dot Matrix Display Module

Materials and tools

3D printer

nuts for screws

Soldering iron kit

Jumper wires (male to female)

32.768 kHz crystal oscillator

Digital multimeter

Adjustable power supply

Software and tools

Arduino IDE

SolidEdge 2025

ChatGPT

Google Gemini

Project description

Project Overview:

Main features

Power architecture

Wiring table

Real-time clock (RTC) circuit

Here are the essential components of the internal RTC circuit:

When transferring MP3 files to the 8GB SD card used with the DFPlayer, strictly follow the recommended guidelines to ensure stable and smooth system performance.

Component Description Quantity

Automatic School Bell V3

Automatic School Bell V3.0

1#include <Wire.h>
2#include <LiquidCrystal_I2C.h>
3#include <LedControl.h>
4#include <string.h>
5#include <STM32RTC.h>
6#include <DFRobotDFPlayerMini.h>
7
8/* ================= BUTTON STRUCT ================= */
9struct ButtonState {
10 bool last;
11 unsigned long lastTime;
12};
13
14ButtonState btnSet = {true, 0};
15ButtonState btnUp = {true, 0};
16ButtonState btnDown = {true, 0};
17
18/* ================= HARDWARE ================= */
19#define BTN_SET PB14
20#define BTN_UP PB13
21#define BTN_DOWN PB12
22
23#define EEPROM_ADDR 0x50
24#define MAX_BELLS 20
25#define TT_NAME_LEN 12
26#define EEPROM_BASE 0
27
28
29#define EEPROM_TTCOUNT_L 0
30#define EEPROM_TTCOUNT_H 1
31#define EEPROM_DEFAULT_TT 2
32#define EEPROM_TT_START 3
33
34
35/* ---------- 8x8 MAX7219 ---------- */
36#define MATRIX_DIN PA7
37#define MATRIX_CLK PA5
38#define MATRIX_CS PA4
39
40#define BUZZER_PIN PA2
41#define RELAY_PIN PA1
42
43
44
45/* -------- DFPLAYER -------- */
46#define MP3_BUSY_PIN PB1
47HardwareSerial mp3Serial(PA10, PA9); // RX, TX
48
49/////////////////////// Start Exam timer //////////////
50bool examRunning = false;
51bool examScheduled = false;
52uint32_t examStartSec = 0;
53uint32_t examDurationSec = 0;
54
55
56//examScheduled = true;
57
58
59uint8_t examStartH = 0;
60uint8_t examStartM = 0;
61
62uint8_t examDurH = 3; // default 3 hours
63uint8_t examDurM = 0;
64uint16_t selectedTimetableIndex = 0;
65
66//////////////////////End Exam Timer
67/* ================= UI ================= */
68enum UiState {
69 UI_DASHBOARD,
70 UI_MENU,
71 UI_SET_DATETIME,
72 UI_SET_BELL,
73 UI_MANAGE_Timetable,
74 UI_CLEAR_EEPROM,
75 UI_TRIGGER_BELL,
76 UI_EXAM_TIMER,
77 UI_AUTO_BELLS,
78 UI_SELFTEST
79};
80
81UiState uiState = UI_DASHBOARD;
82bool screenChanged = true;
83
84
85//HardwareSerial &mp3Serial = Serial1;
86DFRobotDFPlayerMini mp3;
87
88/* ================= BELL TYPE ================= */
89enum BellType : uint8_t
90{
91 BELL_PERIOD = 0,
92 BELL_PRAYER = 1,
93 BELL_RECESS = 2,
94 BELL_END = 3
95};
96
97LedControl matrix = LedControl(MATRIX_DIN, MATRIX_CLK, MATRIX_CS, 1);
98LiquidCrystal_I2C lcd(0x27, 20, 4);
99
100STM32RTC& rtc = STM32RTC::getInstance();
101
102/* ================= DATA ================= */
103uint8_t bellCount = 0;
104uint8_t bellHour[MAX_BELLS];
105uint8_t bellMin[MAX_BELLS];
106uint8_t bellType[MAX_BELLS];
107
108
109bool ttInfoEmbedded = false;
110bool bellActive = false;
111
112/* ---- mp3 runtime ---- */
113bool mp3Playing = false;
114int activeBellIndex = -1;
115
116/* ================= Time Table ================= */
117void readTimetableName(uint16_t index, char *out)
118{
119 if (index >= getTimetableCount())
120 {
121 out[0] = 0;
122 return;
123 }
124
125 uint16_t addr = getTimetableAddress(index);
126
127 for (uint8_t i = 0; i < TT_NAME_LEN; i++)
128 out[i] = eepromReadByte(addr + i);
129
130 out[TT_NAME_LEN] = 0;
131}
132
133
134
135
136
137
138
139/* ================= Time Table ================= */
140bool deleteTimetable(uint16_t index)
141{
142 uint16_t total = getTimetableCount();
143 if (total == 0 || index >= total) return false;
144
145 uint16_t addrDel = getTimetableAddress(index);
146 uint16_t addrNext = getTimetableAddress(index + 1);
147 uint16_t addrEnd = getTimetableAddress(total);
148
149 // Shift EEPROM data to close the gap
150 for (uint16_t a = addrNext; a < addrEnd; a++)
151 {
152 uint8_t v = eepromReadByte(a);
153 eepromWriteByte(addrDel++, v);
154 }
155
156 setTimetableCount(total - 1);
157
158 uint8_t def = getDefaultTimetable();
159 if (def == index)
160 {
161 setDefaultTimetable(0); // Reset to first if current was deleted
162 }
163 else if (def > index)
164 {
165 setDefaultTimetable(def - 1); // Shift index down
166 }
167
168 // Reload the current default into RAM so the bells update immediately
169 loadTimetable(getDefaultTimetable());
170
171 return true;
172}
173
174/* ================= Time Table ================= */
175uint8_t getTimetableBellCount(uint16_t index)
176{
177 uint16_t addr = getTimetableAddress(index);
178 addr += TT_NAME_LEN;
179 return eepromReadByte(addr);
180}
181/* ================= Time Table ================= */
182void readTimetableBell(uint16_t ttIndex,
183 uint8_t bellIndex,
184 uint8_t &h,
185 uint8_t &m,
186 uint8_t &t)
187{
188 uint16_t addr = getTimetableAddress(ttIndex);
189
190 addr += TT_NAME_LEN; // skip name
191 uint8_t cnt = eepromReadByte(addr++);
192 if (bellIndex >= cnt) return;
193
194 addr += bellIndex * 3;
195
196 h = eepromReadByte(addr++);
197 m = eepromReadByte(addr++);
198 t = eepromReadByte(addr++);
199}
200/* ================= Time Table ================= */
201void screenTimetableInfo(uint16_t ttIndex)
202{
203 static uint8_t top = 0;
204
205 if (screenChanged)
206 {
207 top = 0;
208 lcd.clear();
209 lcd.noBlink();
210 screenChanged = false;
211 }
212
213 uint8_t total = getTimetableBellCount(ttIndex);
214
215 char name[TT_NAME_LEN+1];
216 readTimetableName(ttIndex, name);
217
218 char line[21];
219
220 // row 0 – name
221 snprintf(line, sizeof(line), "TT: %s", name);
222 lcdPrintRow(0, line);
223
224 // rows 1 & 2 – two bells per page
225 for (uint8_t r = 0; r < 2; r++)
226 {
227 uint8_t i = top + r;
228
229 if (i < total)
230 {
231 uint8_t h,m,t;
232 readTimetableBell(ttIndex, i, h, m, t);
233
234 char c;
235 if (t == BELL_PERIOD) c = 'C';
236 else if (t == BELL_PRAYER) c = 'P';
237 else if (t == BELL_RECESS) c = 'R';
238 else if (t == BELL_END) c = 'E';
239 else c = '?';
240
241 snprintf(line, sizeof(line),
242 "B%02d %02d:%02d %c",
243 i+1, h, m, c);
244
245 lcdPrintRow(1+r, line);
246 }
247 else
248 lcdPrintRow(1+r, "");
249 }
250
251 lcdPrintRow(3, "UP/DN SCROLL SET");
252
253 if (readButton(BTN_DOWN, btnDown))
254 {
255 if (top + 2 < total) top++;
256 }
257
258 if (readButton(BTN_UP, btnUp))
259 {
260 if (top > 0) top--;
261 }
262
263 if (readButton(BTN_SET, btnSet))
264 {
265 uiState = UI_MENU; // or back to list (see note below)
266 screenChanged = true;
267 }
268}
269/* ================= Time Table ================= */
270/* ================= Time Table ================= */
271/* ================= Time Table ================= */
272/* ================= Time Table ================= */
273/* ================= Time Table ================= */
274/* ================= Time Table ================= */
275/* ================= Time Table ================= */
276/* ================= Time Table ================= */
277
278void eepromWriteBlock(uint16_t addr, const uint8_t *buf, uint16_t len)
279{
280 for (uint16_t i = 0; i < len; i++)
281 eepromWriteByte(addr + i, buf[i]);
282}
283
284void eepromReadBlock(uint16_t addr, uint8_t *buf, uint16_t len)
285{
286 for (uint16_t i = 0; i < len; i++)
287 buf[i] = eepromReadByte(addr + i);
288}
289
290uint16_t getTimetableCount()
291{
292 uint16_t v = eepromReadByte(EEPROM_TTCOUNT_L);
293 v |= (uint16_t)eepromReadByte(EEPROM_TTCOUNT_H) << 8;
294 return v;
295}
296
297void setTimetableCount(uint16_t c)
298{
299 eepromWriteByte(EEPROM_TTCOUNT_L, c & 0xFF);
300 eepromWriteByte(EEPROM_TTCOUNT_H, (c >> 8) & 0xFF);
301}
302
303uint8_t getDefaultTimetable()
304{
305 return eepromReadByte(EEPROM_DEFAULT_TT);
306}
307
308void setDefaultTimetable(uint8_t index)
309{
310 eepromWriteByte(EEPROM_DEFAULT_TT, index);
311}
312
313
314
315uint16_t getTimetableAddress(uint16_t index)
316{
317 uint16_t addr = EEPROM_TT_START;
318
319 uint16_t total = getTimetableCount();
320 if (index >= total) index = total;
321
322 for (uint16_t i = 0; i < index; i++)
323 {
324 addr += TT_NAME_LEN;
325
326 uint8_t cnt = eepromReadByte(addr);
327
328 // ---------- HARD SAFETY ----------
329 if (cnt > MAX_BELLS)
330 cnt = 0;
331 // --------------------------------
332
333 addr += 1;
334 addr += cnt * 3;
335 }
336
337 return addr;
338}
339
340
341const char *monthName(uint8_t m)
342{
343 static const char *names[] =
344 {"JAN","FEB","MAR","APR","MAY","JUN",
345 "JUL","AUG","SEP","OCT","NOV","DEC"};
346
347 if (m < 1 || m > 12) return "UNK";
348 return names[m-1];
349}
350
351uint8_t countMonthTimetables(const char *base)
352{
353 uint16_t n = getTimetableCount();
354 uint8_t cnt = 0;
355
356 for(uint16_t i=0;i<n;i++)
357 {
358 char name[TT_NAME_LEN+1];
359
360 uint16_t addr = getTimetableAddress(i);
361
362 for(uint8_t j=0;j<TT_NAME_LEN;j++)
363 name[j] = eepromReadByte(addr+j);
364
365 name[TT_NAME_LEN] = 0;
366
367 if (strncmp(name, base, 3) == 0)
368 cnt++;
369 }
370 return cnt;
371}
372void makeAutoTimetableName(char *out)
373{
374 uint16_t cnt = getTimetableCount();
375
376 // first ever timetable
377 if (cnt == 0)
378 {
379 strcpy(out, "DEFAULT");
380 return;
381 }
382
383 uint8_t m = rtc.getMonth();
384 const char *base = monthName(m);
385
386 uint8_t already = countMonthTimetables(base);
387
388 if (already == 0)
389 {
390 strcpy(out, base);
391 }
392 else
393 {
394 snprintf(out, TT_NAME_LEN+1, "%s_%d", base, already+1);
395 }
396}
397
398bool saveCurrentTimetable(const char *name)
399{
400 uint16_t ttCount = getTimetableCount();
401
402 uint16_t addr = getTimetableAddress(ttCount);
403
404 // name
405 for (uint8_t i = 0; i < TT_NAME_LEN; i++)
406 {
407 uint8_t c = 0;
408 if (i < strlen(name)) c = name[i];
409 eepromWriteByte(addr++, c);
410 }
411
412 // bell count
413 eepromWriteByte(addr++, bellCount);
414
415 // bells
416 for (uint8_t i = 0; i < bellCount; i++)
417 {
418 eepromWriteByte(addr++, bellHour[i]);
419 eepromWriteByte(addr++, bellMin[i]);
420 eepromWriteByte(addr++, bellType[i]);
421 }
422
423 setTimetableCount(ttCount + 1);
424
425 // only first timetable becomes default
426 if (ttCount == 0)
427 {
428 setDefaultTimetable(0);
429 }
430
431 return true;
432}
433
434
435
436
437
438
439bool loadTimetable(uint16_t index)
440{
441 if (index >= getTimetableCount())
442 return false;
443
444 uint16_t addr = getTimetableAddress(index);
445
446 // skip name
447 addr += TT_NAME_LEN;
448
449 uint8_t cnt = eepromReadByte(addr++);
450
451 if (cnt > MAX_BELLS) return false;
452
453 bellCount = cnt;
454
455 for (uint8_t i = 0; i < bellCount; i++)
456 {
457 bellHour[i] = eepromReadByte(addr++);
458 bellMin[i] = eepromReadByte(addr++);
459 bellType[i] = eepromReadByte(addr++);
460 }
461
462 return true;
463}
464
465
466/* ================= Time Table ================= */
467
468
469/* ================= FONT ================= */
470// index mapping
471// 0..9 -> digits 0..9
472// 10 -> C
473// 11 -> R
474// 12 -> P
475// 13 -> E
476
477const byte font8x8[][8] =
478{
479/* 0 */
480{0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00},
481/* 1 */
482{0x18,0x38,0x18,0x18,0x18,0x18,0x3C,0x00},
483/* 2 */
484{0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00},
485/* 3 */
486{0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
487/* 4 */
488{0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00},
489/* 5 */
490{0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
491/* 6 */
492{0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00},
493/* 7 */
494{0x7E,0x66,0x06,0x0C,0x18,0x18,0x18,0x00},
495/* 8 */
496{0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00},
497/* 9 */
498{0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
499
500/* 10 = C */
501{0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00},
502
503/* 11 = R */
504{0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
505
506/* 12 = P */
507{0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
508
509/* 13 = E */
510{0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}
511};
512
513
514/* ================= MATRIX ================= */
515void drawChar8x8(byte index)
516{
517 matrix.clearDisplay(0);
518
519 for (int row = 0; row < 8; row++)
520 {
521 // vertical flip
522 byte line = font8x8[index][7 - row];
523
524 for (int col = 0; col < 8; col++)
525 {
526 // horizontal flip
527 bool on = line & (1 << col);
528
529 matrix.setLed(0, row, col, on);
530 }
531 }
532}
533
534
535
536/* ================= MATRIX ================= */
537/* ================= MATRIX ================= */
538void showCountdownOnMatrix(long sec)
539{
540 if (sec < 0) {
541 matrix.clearDisplay(0);
542 return;
543 }
544
545 if (sec > 60) sec = 60;
546
547 int cols = map(sec, 0, 60, 0, 8);
548
549 matrix.clearDisplay(0);
550
551 for (int c = 0; c < cols; c++) {
552 for (int r = 0; r < 8; r++) {
553 matrix.setLed(0, r, 7 - c, true);
554 }
555 }
556}
557
558/* ================= INDICATOR ================= */
559void updateCountdownIndicators(long countdown)
560{
561 static unsigned long t = 0;
562 static bool blink = false;
563
564 if (bellCount == 0 || countdown < 0) {
565 matrix.clearDisplay(0);
566 return;
567 }
568
569 if (countdown <= 10 && countdown >= 1)
570 {
571 if (millis() - t > 200) {
572 t = millis();
573 blink = !blink;
574 }
575
576 if (blink) showCountdownOnMatrix(countdown);
577 else matrix.clearDisplay(0);
578
579 return;
580 }
581
582 if (countdown <= 60) {
583 showCountdownOnMatrix(countdown);
584 return;
585 }
586
587 matrix.clearDisplay(0);
588}
589
590
591/* ================= Time Table ================= */
592/* ================= Time Table ================= */
593
594void screenTimetableList() {
595 static enum { L_LIST, L_INFO, L_CONFIRM } layer = L_LIST;
596 static uint16_t sel = 0;
597 static uint16_t top = 0;
598 static uint8_t infoTop = 0;
599 static uint8_t operation = 0; // 1=Delete, 2=Default
600 static bool waitRelease = false;
601
602 uint16_t total = getTimetableCount();
603
604 // 1. Safety: Wait for button release
605 if (waitRelease) {
606 if (digitalRead(BTN_SET) == HIGH && digitalRead(BTN_UP) == HIGH && digitalRead(BTN_DOWN) == HIGH) {
607 waitRelease = false;
608 }
609 return;
610 }
611
612 // 2. Screen Reset
613 if (screenChanged) {
614 layer = L_LIST; sel = 0; top = 0;
615 lcd.clear(); lcd.noBlink();
616 screenChanged = false;
617 }
618
619 // 3. If EEPROM is empty
620 if (total == 0) {
621 lcdPrintRow(0, "NO TIMETABLES");
622 lcdPrintRow(3, "SET = BACK");
623 if (readButton(BTN_SET, btnSet)) { uiState = UI_MENU; screenChanged = true; }
624 return;
625 }
626
627 char name[TT_NAME_LEN + 1];
628 readTimetableName(sel, name);
629
630 /* ================= LAYER: CONFIRMATION (YES/NO) ================= */
631 if (layer == L_CONFIRM) {
632 lcdPrintRow(0, (operation == 1) ? "DELETE TABLE?" : "SET AS DEFAULT?");
633 lcdPrintRow(1, name);
634 lcdPrintRow(3, "SET=YES UP=NO");
635
636 if (readButton(BTN_UP, btnUp)) { // User chose NO
637 layer = L_INFO;
638 lcd.clear();
639 }
640 if (readButton(BTN_SET, btnSet)) { // User chose YES
641 if (operation == 1) {
642 deleteTimetable(sel);
643 total = getTimetableCount();
644 if (sel >= total && total > 0) sel = total - 1;
645 layer = L_LIST;
646 } else {
647 setDefaultTimetable(sel);
648 loadTimetable(sel);
649 layer = L_INFO;
650 }
651 waitRelease = true;
652 lcd.clear();
653 }
654 return;
655 }
656
657 /* ================= LAYER: DETAIL INFO (Bell List) ================= */
658 if (layer == L_INFO) {
659 uint8_t bellTotal = getTimetableBellCount(sel);
660 lcdPrintRow(0, name);
661
662 for (uint8_t r = 0; r < 2; r++) {
663 uint8_t i = infoTop + r;
664 if (i < bellTotal) {
665 uint8_t h, m, t; readTimetableBell(sel, i, h, m, t);
666
667 // Convert Type Number to Text
668 const char* typeText = "PERIOD";
669 if (t == 1) typeText = "PRAYER";
670 else if (t == 2) typeText = "RECESS";
671 else if (t == 3) typeText = "END";
672
673 char line[21];
674 snprintf(line, sizeof(line), "%02d:%02d %s", h, m, typeText);
675 lcdPrintRow(r + 1, line);
676 } else lcdPrintRow(r + 1, "");
677 }
678 lcdPrintRow(3, "UP/DN=SCR SET=BACK");
679
680 if (readButton(BTN_DOWN, btnDown) && infoTop + 1 < bellTotal) infoTop++;
681 if (readButton(BTN_UP, btnUp) && infoTop > 0) infoTop--;
682
683 // COMBOS for Delete/Default
684 if (digitalRead(BTN_SET) == LOW) {
685 if (digitalRead(BTN_UP) == LOW) { operation = 2; layer = L_CONFIRM; waitRelease = true; lcd.clear(); return; }
686 if (digitalRead(BTN_DOWN) == LOW) { operation = 1; layer = L_CONFIRM; waitRelease = true; lcd.clear(); return; }
687 }
688
689 if (readButton(BTN_SET, btnSet)) { layer = L_LIST; infoTop = 0; lcd.clear(); }
690 return;
691 }
692
693 /* ================= LAYER: LIST VIEW (Main Selection) ================= */
694 if (readButton(BTN_DOWN, btnDown) && sel + 1 < total) sel++;
695 if (readButton(BTN_UP, btnUp)) {
696 if (sel > 0) sel--;
697 else { // If user presses UP at the top of the list, go back to Menu
698 uiState = UI_MENU;
699 screenChanged = true;
700 return;
701 }
702 }
703
704 if (sel < top) top = sel;
705 if (sel >= top + 2) top = sel - 1;
706
707 lcdPrintRow(0, " SELECT TABLE");
708 uint8_t defIdx = getDefaultTimetable();
709
710 for (uint8_t r = 0; r < 2; r++) {
711 uint16_t idx = top + r;
712 if (idx < total) {
713 char tName[TT_NAME_LEN + 1]; readTimetableName(idx, tName);
714 char line[21];
715 snprintf(line, sizeof(line), "%c %-12s %c", (idx == sel) ? '>' : ' ', tName, (idx == defIdx) ? '*' : ' ');
716 lcdPrintRow(r + 1, line);
717 } else lcdPrintRow(r + 1, "");
718 }
719 lcdPrintRow(3, "SET=OPEN UP=EXIT");
720
721 if (readButton(BTN_SET, btnSet)) { layer = L_INFO; lcd.clear(); }
722}
723
724
725
726/* ================= Time Table ================= */
727/* ================= Time Table ================= */
728
729
730
731
732
733
734
735
736
737
738///////////////////////
739////////////////////////
740/////////////////////////
741
742
743
744/* ================= Time Table ================= */
745/* ================= EEPROM ================= */
746void eepromWriteByte(uint16_t addr, uint8_t data) {
747 Wire.beginTransmission(EEPROM_ADDR);
748 Wire.write((uint8_t)(addr >> 8));
749 Wire.write((uint8_t)(addr & 0xFF));
750 Wire.write(data);
751 Wire.endTransmission();
752 delay(10);
753}
754
755uint8_t eepromReadByte(uint16_t addr) {
756 Wire.beginTransmission(EEPROM_ADDR);
757 Wire.write(addr >> 8);
758 Wire.write(addr & 0xFF);
759 Wire.endTransmission();
760 Wire.requestFrom(EEPROM_ADDR, (uint8_t)1);
761 return Wire.available() ? Wire.read() : 0;
762}
763
764/* ================= BUTTON ================= */
765bool readButton(uint8_t pin, ButtonState &b)
766{
767 bool now = digitalRead(pin);
768
769 if (now != b.last) {
770 b.lastTime = millis();
771 b.last = now;
772 }
773
774 if ((millis() - b.lastTime) > 40) {
775 if (now == LOW) {
776 static unsigned long lock[3] = {0,0,0};
777
778 unsigned long *lk;
779 if (pin == BTN_SET) lk = &lock[0];
780 else if (pin == BTN_UP) lk = &lock[1];
781 else lk = &lock[2];
782
783 if (millis() - *lk > 300) {
784 *lk = millis();
785 return true;
786 }
787 }
788 }
789 return false;
790}
791
792/* ================= LCD ================= */
793void lcdPrintRow(uint8_t row, const char *txt) {
794 lcd.setCursor(0, row);
795 lcd.print(txt);
796 for (int i = strlen(txt); i < 20; i++) lcd.print(' ');
797}
798
799void showError(const char* line1, const char* line2) {
800 lcd.clear();
801 lcdPrintRow(1, line1);
802 lcdPrintRow(2, line2);
803 delay(1500);
804}
805
806/* ================= TIME / BELL ================= */
807int timeToMinutes(uint8_t h, uint8_t m) {
808 return h * 60 + m;
809}
810
811void getBellStatus(int &currentBell, int &nextBell, long &countdownSec) {
812
813 currentBell = -1;
814 nextBell = -1;
815 countdownSec = -1;
816
817 if (bellCount == 0) return;
818
819 int nowMin = timeToMinutes(rtc.getHours(), rtc.getMinutes());
820 int nowSec = rtc.getSeconds();
821
822 for (int i = 0; i < bellCount; i++) {
823
824 int bellTimeMin = timeToMinutes(bellHour[i], bellMin[i]);
825
826 if (nowMin > bellTimeMin || (nowMin == bellTimeMin && nowSec > 0)) {
827 currentBell = i;
828 }
829 else {
830 nextBell = i;
831 int deltaMin = bellTimeMin - nowMin;
832 countdownSec = deltaMin * 60 - nowSec;
833 return;
834 }
835 }
836
837 currentBell = -1;
838 nextBell = -1;
839 countdownSec = -1;
840}
841
842/* ================= BELL HANDLER ================= */
843void handleBell()
844{
845 static unsigned long bellStart = 0;
846 static bool timerStarted = false;
847
848 if (bellActive && !timerStarted)
849 {
850 bellStart = millis();
851 timerStarted = true;
852 }
853
854 if (bellActive && timerStarted)
855 {
856 if (millis() - bellStart >= 3000)
857 {
858 digitalWrite(RELAY_PIN, HIGH);
859 digitalWrite(BUZZER_PIN, LOW);
860
861 bellActive = false;
862 timerStarted = false;
863 }
864 }
865}
866
867
868
869/* ================= I2C SCAN ================= */
870void scanI2C() {
871 for (byte a = 1; a < 127; a++) {
872 Wire.beginTransmission(a);
873 if (Wire.endTransmission() == 0) {
874 lcd.clear();
875 lcdPrintRow(0, "I2C found:");
876 lcd.setCursor(0,1);
877 lcd.print(a, HEX);
878 delay(300);
879 }
880 }
881}
882
883/* ================= DASHBOARD ================= */
884char getBellSymbolChar(int index)
885{
886 if (index < 0 || index >= bellCount) return '-';
887
888 if (bellType[index] == BELL_PRAYER) return 'P';
889 if (bellType[index] == BELL_RECESS) return 'R';
890 if (bellType[index] == BELL_END) return 'E';
891
892 // PERIOD
893 return 'C'; // N = normal / period
894}
895
896
897
898/* ================= DASHBOARD ================= */
899
900
901/* ================= DASHBOARD ================= */
902void screenDashboard() {
903
904 if (screenChanged) {
905 lcd.clear();
906 lcd.noBlink();
907 screenChanged = false;
908 }
909
910 char line0[21];
911
912 snprintf(line0, sizeof(line0),
913 "%02d/%02d/%04d %02d:%02d:%02d",
914 rtc.getDay(),
915 rtc.getMonth(),
916 2000 + rtc.getYear(),
917 rtc.getHours(),
918 rtc.getMinutes(),
919 rtc.getSeconds());
920
921 lcdPrintRow(0, line0);
922
923 int curBell, nextBell;
924 long countdown;
925
926 getBellStatus(curBell, nextBell, countdown);
927
928if (curBell >= 0)
929// snprintf(line0, sizeof(line0),"Current Bell:%2d %c",curBell + 1,getBellSymbolChar(curBell));
930{
931 uint8_t t = bellType[curBell];
932 int periodNumber = 0;
933
934 // Calculate period number exactly like the Matrix logic does
935 for (int i = 0; i <= curBell; i++) {
936 if (bellType[i] == BELL_PERIOD) {
937 periodNumber++;
938 }
939 }
940
941 if (t == BELL_PRAYER) {
942 snprintf(line0, sizeof(line0), "Current: PRAYER [P]");
943 } else if (t == BELL_RECESS) {
944 snprintf(line0, sizeof(line0), "Current: RECESS [R]");
945 } else if (t == BELL_END) {
946 snprintf(line0, sizeof(line0), "Current: SCHOOL OFF[E]");
947 } else {
948 // This matches the 1, 2, 3... shown on your Matrix
949 snprintf(line0, sizeof(line0), "Current: PERIOD %d", periodNumber);
950 }
951} else {
952 snprintf(line0, sizeof(line0), "Current: --");
953}
954lcdPrintRow(1, line0);
955//else
956 //snprintf(line0, sizeof(line0), "Current Bell: --");
957
958//lcdPrintRow(1, line0);
959
960
961
962
963
964
965if (nextBell >= 0)
966 snprintf(line0, sizeof(line0),
967 "Next Bell: %02d:%02d %c",
968 bellHour[nextBell],
969 bellMin[nextBell],
970 getBellSymbolChar(nextBell));
971else
972 snprintf(line0, sizeof(line0), "Next Bell: --:--");
973
974lcdPrintRow(2, line0);
975
976 if (countdown >= 0) {
977
978 int hh = countdown / 3600;
979 int mm = (countdown % 3600) / 60;
980 int ss = countdown % 60;
981
982 snprintf(line0, sizeof(line0),
983 "In: %02d:%02d:%02d", hh, mm, ss);
984 }
985 else {
986 snprintf(line0, sizeof(line0), "In: --:--:--");
987 }
988
989 lcdPrintRow(3, line0);
990
991 if (readButton(BTN_SET, btnSet)) {
992 uiState = UI_MENU;
993 screenChanged = true;
994 }
995}
996
997/* ================= MENU ================= */
998/* ================= MENU ================= */
999void screenMenu()
1000{
1001 static int sel = 0;
1002 static int top = 0;
1003 static bool waitRelease = false;
1004
1005 const char* items[] = {
1006 "Set Date & Time",
1007 "Set Bell",
1008 "Manage Timetable",
1009 "Clear EEPROM",
1010 "Trigger Bell Now",
1011 "Start Exam Timer",
1012 "Auto Generate Bells",
1013 "Self Test",
1014 "Return"
1015 };
1016
1017 const int ITEM_COUNT = 9;
1018 const int VISIBLE = 3;
1019
1020 /* -------- screen entry -------- */
1021 if (screenChanged)
1022 {
1023 sel = 0;
1024 top = 0;
1025 waitRelease = true;
1026
1027 lcd.clear();
1028 lcd.noBlink();
1029 screenChanged = false;
1030 }
1031
1032 /* -------- wait for key release -------- */
1033 if (waitRelease)
1034 {
1035 if (digitalRead(BTN_SET) == HIGH &&
1036 digitalRead(BTN_UP) == HIGH &&
1037 digitalRead(BTN_DOWN) == HIGH)
1038 {
1039 waitRelease = false;
1040 }
1041 return;
1042 }
1043
1044 /* -------- navigation -------- */
1045 if (readButton(BTN_DOWN, btnDown) && sel < ITEM_COUNT - 1) sel++;
1046 if (readButton(BTN_UP, btnUp) && sel > 0) sel--;
1047
1048 if (sel < top) top = sel;
1049 if (sel >= top + VISIBLE) top = sel - VISIBLE + 1;
1050
1051 /* -------- render -------- */
1052 lcdPrintRow(0, "MENU");
1053
1054 for (int r = 0; r < VISIBLE; r++)
1055 {
1056 int idx = top + r;
1057 char line[21];
1058
1059 if (idx < ITEM_COUNT)
1060 {
1061 snprintf(line, sizeof(line),
1062 "%c %s",
1063 (idx == sel) ? '>' : ' ',
1064 items[idx]);
1065 lcdPrintRow(r + 1, line);
1066 }
1067 else
1068 {
1069 lcdPrintRow(r + 1, "");
1070 }
1071 }
1072
1073 /* -------- select -------- */
1074 if (readButton(BTN_SET, btnSet))
1075 {
1076 screenChanged = true;
1077
1078 switch (sel)
1079 {
1080 case 0: uiState = UI_SET_DATETIME; break;
1081 case 1: uiState = UI_SET_BELL; break;
1082 case 2: uiState = UI_MANAGE_Timetable; break;
1083 case 3: uiState = UI_CLEAR_EEPROM; break;
1084 case 4: uiState = UI_TRIGGER_BELL; break;
1085 case 5: uiState = UI_EXAM_TIMER; break;
1086 case 6: uiState = UI_AUTO_BELLS; break;
1087 case 7: uiState = UI_SELFTEST; break;
1088 case 8: uiState = UI_DASHBOARD; break;
1089 }
1090 return;
1091 }
1092}
1093
1094/* ================= PLACEHOLDER ================= */
1095void placeholderScreen(const char* title) {
1096
1097 if (screenChanged) {
1098 lcd.clear();
1099 lcd.noBlink();
1100 screenChanged = false;
1101 }
1102
1103 lcdPrintRow(0, title);
1104 lcdPrintRow(1, "Feature Coming");
1105 lcdPrintRow(2, "Soon...");
1106 lcdPrintRow(3, "SET = BACK");
1107
1108 if (readButton(BTN_SET, btnSet)) {
1109 uiState = UI_MENU;
1110 screenChanged = true;
1111 }
1112}
1113
1114/* ================= SET DATE/TIME ================= */
1115void screenSetDateTime()
1116{
1117 static int step, d, m, y, h, mi;
1118
1119 if (screenChanged)
1120 {
1121 d = rtc.getDay();
1122 m = rtc.getMonth();
1123 y = 2000 + rtc.getYear();
1124 h = rtc.getHours();
1125 mi = rtc.getMinutes();
1126
1127 step = 0;
1128 lcd.clear();
1129 lcd.blink();
1130 screenChanged = false;
1131 }
1132
1133 char l1[21], l2[21];
1134
1135 snprintf(l1, sizeof(l1), "%02d %02d %04d", d, m, y);
1136 snprintf(l2, sizeof(l2), "%02d %02d", h, mi);
1137
1138 lcdPrintRow(0, "SET DATE & TIME");
1139 lcdPrintRow(1, l1);
1140 lcdPrintRow(2, l2);
1141 lcdPrintRow(3, "SET=SAVE");
1142
1143 const uint8_t cx[] = {0,3,6,0,3};
1144 const uint8_t cy[] = {1,1,1,2,2};
1145 lcd.setCursor(cx[step], cy[step]);
1146
1147 if (readButton(BTN_UP, btnUp)) {
1148 if (step == 0 && d < 31) d++;
1149 else if (step == 1 && m < 12) m++;
1150 else if (step == 2) y++;
1151 else if (step == 3) h = (h + 1) % 24;
1152 else if (step == 4) mi = (mi + 1) % 60;
1153 }
1154
1155 if (readButton(BTN_DOWN, btnDown)) {
1156 if (step == 0 && d > 1) d--;
1157 else if (step == 1 && m > 1) m--;
1158 else if (step == 2 && y > 2000) y--;
1159 else if (step == 3) h = (h + 23) % 24;
1160 else if (step == 4) mi = (mi + 59) % 60;
1161 }
1162
1163 if (readButton(BTN_SET, btnSet)) {
1164
1165 step++;
1166
1167 if (step > 4) {
1168
1169 lcd.noBlink();
1170 y = y % 100;
1171 rtc.setDate(d, m, y);
1172 rtc.setTime(h, mi, 0);
1173
1174 uiState = UI_MENU;
1175 screenChanged = true;
1176 }
1177 }
1178}
1179
1180/* ================= SET BELL ================= */
1181void screenSetBell()
1182{
1183 static int stage; // 0=count, 1=edit bells, 2=save screen
1184 static int idx;
1185 static int hh, mm;
1186 static int step; // 0=HH,1=MM,2=TYPE
1187 static uint8_t typeSel;
1188
1189 static char ttName[TT_NAME_LEN+1];
1190
1191 if (screenChanged)
1192 {
1193 stage = 0;
1194 idx = 0;
1195 hh = 0;
1196 mm = 0;
1197 step = 0;
1198 typeSel = BELL_PERIOD;
1199
1200 lcd.clear();
1201 lcd.noBlink();
1202 screenChanged = false;
1203 }
1204
1205 /* ================= STAGE 0 : number of bells ================= */
1206 if (stage == 0)
1207 {
1208 lcdPrintRow(0, "SET BELL");
1209 lcdPrintRow(1, "No of Bells");
1210
1211 char cntLine[21];
1212 snprintf(cntLine, sizeof(cntLine), "%02d", bellCount);
1213 lcdPrintRow(2, cntLine);
1214
1215 lcdPrintRow(3, "UP/DN SET=OK");
1216
1217 if (readButton(BTN_UP, btnUp) && bellCount < MAX_BELLS) bellCount++;
1218 if (readButton(BTN_DOWN, btnDown) && bellCount > 0) bellCount--;
1219
1220 if (readButton(BTN_SET, btnSet))
1221 {
1222 if (bellCount == 0)
1223 {
1224 showError("INVALID", "Bell Count");
1225 }
1226 else
1227 {
1228 stage = 1;
1229 idx = 0;
1230 hh = 0;
1231 mm = 0;
1232 step = 0;
1233 typeSel = BELL_PERIOD;
1234
1235 lcd.clear();
1236 lcd.blink();
1237 }
1238 }
1239 return;
1240 }
1241
1242 /* ================= STAGE 2 : save screen ================= */
1243 if (stage == 2)
1244 {
1245 lcdPrintRow(0, "SAVE TIMETABLE ?");
1246
1247 char line[21];
1248 snprintf(line, sizeof(line), "NAME: %s", ttName);
1249 lcdPrintRow(1, line);
1250
1251 lcdPrintRow(3, "SET=SAVE UP=NO");
1252
1253 if (readButton(BTN_SET, btnSet))
1254 {
1255 saveCurrentTimetable(ttName);
1256
1257 showError("TIMETABLE", "SAVED");
1258
1259 uiState = UI_MENU;
1260 screenChanged = true;
1261 return;
1262 }
1263
1264 if (readButton(BTN_UP, btnUp))
1265 {
1266 uiState = UI_MENU;
1267 screenChanged = true;
1268 return;
1269 }
1270
1271 return;
1272 }
1273
1274 /* ================= STAGE 1 : edit bells ================= */
1275
1276 lcdPrintRow(0, "SET BELL");
1277
1278 char title[21];
1279 snprintf(title, sizeof(title), "Bell %d of %d", idx + 1, bellCount);
1280 lcdPrintRow(1, title);
1281
1282 // row 2 : time
1283 char line2[21];
1284 snprintf(line2, sizeof(line2), "%02d:%02d", hh, mm);
1285 lcdPrintRow(2, line2);
1286
1287 // row 3 : type when step==2
1288 if (step == 2)
1289 {
1290 const char* tname[] = {"PERIOD","PRAYER","RECESS","END"};
1291
1292 char line3[21];
1293 snprintf(line3, sizeof(line3), ">%s", tname[typeSel]);
1294 lcdPrintRow(3, line3);
1295
1296 lcd.setCursor(11, 3);
1297 lcd.print("SET=NEXT");
1298
1299 lcd.setCursor(0, 3);
1300 }
1301 else
1302 {
1303 lcdPrintRow(3, "");
1304 lcd.setCursor(11, 3);
1305 lcd.print("SET=NEXT");
1306
1307 if (step == 0) lcd.setCursor(0,2);
1308 else if (step == 1) lcd.setCursor(3,2);
1309 }
1310
1311 /* ---- edit values ---- */
1312
1313 if (readButton(BTN_UP, btnUp))
1314 {
1315 if (step == 0 && hh < 23) hh++;
1316 else if (step == 1 && mm < 59) mm++;
1317 else if (step == 2 && typeSel < BELL_END) typeSel++;
1318 }
1319
1320 if (readButton(BTN_DOWN, btnDown))
1321 {
1322 if (step == 0 && hh > 0) hh--;
1323 else if (step == 1 && mm > 0) mm--;
1324 else if (step == 2 && typeSel > 0) typeSel--;
1325 }
1326
1327 /* ---- confirm current bell ---- */
1328
1329 if (readButton(BTN_SET, btnSet))
1330 {
1331 if (step < 2)
1332 {
1333 step++;
1334 return;
1335 }
1336
1337 // only one END
1338 if (typeSel == BELL_END)
1339 {
1340 for (int i = 0; i < idx; i++)
1341 if (bellType[i] == BELL_END)
1342 {
1343 showError("ONLY ONE","END BELL");
1344 return;
1345 }
1346 }
1347
1348 // only one PRAYER
1349 if (typeSel == BELL_PRAYER)
1350 {
1351 for (int i = 0; i < idx; i++)
1352 if (bellType[i] == BELL_PRAYER)
1353 {
1354 showError("ONLY ONE","PRAYER");
1355 return;
1356 }
1357 }
1358
1359 // time must be increasing
1360 if (idx > 0)
1361 {
1362 int prev = bellHour[idx - 1] * 60 + bellMin[idx - 1];
1363 int curr = hh * 60 + mm;
1364
1365 if (curr <= prev)
1366 {
1367 char err[21];
1368 snprintf(err, sizeof(err),
1369 "Must be > %02d:%02d",
1370 bellHour[idx - 1], bellMin[idx - 1]);
1371
1372 showError("INVALID TIME", err);
1373 step = 0;
1374 return;
1375 }
1376 }
1377
1378 // store this bell
1379 bellHour[idx] = hh;
1380 bellMin[idx] = mm;
1381 bellType[idx] = typeSel;
1382
1383 idx++;
1384 step = 0;
1385 hh = 0;
1386 mm = 0;
1387 typeSel = BELL_PERIOD;
1388
1389 /* ---- last bell reached ---- */
1390 if (idx == bellCount)
1391 {
1392 lcd.noBlink();
1393
1394 makeAutoTimetableName(ttName);
1395
1396 stage = 2; // go to save screen
1397 lcd.clear();
1398 return;
1399 }
1400
1401 // prepare next bell entry
1402 lcd.clear();
1403 lcd.blink();
1404 }
1405}
1406
1407
1408
1409/* ================= SELF TEST ================= */
1410void screenSelfTest() {
1411 static uint8_t step = 0;
1412 static uint8_t subStep = 1;
1413 static unsigned long lastUpdate = 0;
1414
1415 // --- INITIALIZATION ---
1416 if (screenChanged) {
1417 step = 0;
1418 subStep = 1;
1419 lastUpdate = millis();
1420 lcd.clear();
1421 matrix.clearDisplay(0);
1422
1423 // Safety: Reset outputs to Idle states
1424 digitalWrite(RELAY_PIN, HIGH); // OFF (Active Low)
1425 digitalWrite(BUZZER_PIN, LOW); // OFF (Active High)
1426
1427 mp3.stop();
1428 screenChanged = false;
1429 }
1430
1431 // --- EXIT LOGIC ---
1432 // Press SET to exit only after the full test is complete
1433 if (step == 10 && readButton(BTN_SET, btnSet)) {
1434 uiState = UI_MENU;
1435 screenChanged = true;
1436 return;
1437 }
1438
1439 // Standard interval for automatic hardware steps
1440 if (step < 6 && (millis() - lastUpdate < 2000)) return;
1441 lastUpdate = millis();
1442
1443 switch (step) {
1444 case 0: // 1. I2C & RTC
1445 lcdPrintRow(0, "1. I2C/RTC TEST");
1446 Wire.beginTransmission(EEPROM_ADDR);
1447 lcdPrintRow(1, (Wire.endTransmission() == 0) ? "EEPROM: OK (0x50)" : "EEPROM: NOT FOUND");
1448 lcdPrintRow(2, (rtc.getYear() > 0) ? "RTC: RUNNING" : "RTC: NOT INIT");
1449 break;
1450
1451 case 1: // 2. Relay Test
1452 lcdPrintRow(0, "2. RELAY TEST");
1453 lcdPrintRow(1, "RELAY (A1) ON...");
1454 digitalWrite(RELAY_PIN, LOW); // ON
1455 break;
1456
1457 case 2: // Stop Relay
1458 digitalWrite(RELAY_PIN, HIGH); // OFF
1459 lcdPrintRow(1, "RELAY (A1) OFF");
1460 break;
1461
1462 case 3: // 3. Buzzer Test
1463 lcdPrintRow(0, "3. BUZZER TEST");
1464 lcdPrintRow(1, "BUZZER (A2) ON...");
1465 digitalWrite(BUZZER_PIN, HIGH); // ON
1466 break;
1467
1468 case 4: // Stop Buzzer
1469 digitalWrite(BUZZER_PIN, LOW); // OFF
1470 lcdPrintRow(1, "BUZZER OFF");
1471 break;
1472
1473 case 5: // 4. Matrix Test
1474 lcdPrintRow(0, "4. MATRIX TEST");
1475 lcdPrintRow(1, "FILLING...");
1476
1477 for (int i = 0; i < 8; i++) {
1478 for (int j = 0; j < 8; j++) {
1479 matrix.setLed(0, i, j, true);
1480 }
1481 }
1482
1483 delay(5000); // 5 seconds ON time
1484
1485 matrix.clearDisplay(0);
1486
1487 step = 6;
1488 return;
1489
1490 case 6: // 5. Audio Scan Setup
1491 matrix.clearDisplay(0);
1492 lcd.clear();
1493 lcdPrintRow(0, "5. AUDIO SCAN");
1494 lcdPrintRow(3, "SET = SKIP TRACK");
1495 subStep = 1;
1496 step = 7;
1497 return;
1498
1499 case 7: // 5. Audio Execution (Tracks 1-14)
1500
1501 static bool trackStarted = false;
1502 static unsigned long trackStartTime = 0;
1503
1504 if (subStep <= 17) {
1505
1506 char buf[20];
1507 snprintf(buf, sizeof(buf), "PLAYING: %02d.mp3", subStep);
1508 lcdPrintRow(1, buf);
1509
1510 // ---- Start Track Only Once ----
1511 if (!trackStarted && digitalRead(MP3_BUSY_PIN) == HIGH) {
1512
1513 mp3.playMp3Folder(subStep);
1514 trackStarted = true;
1515 trackStartTime = millis();
1516 delay(200); // small stability delay
1517 return;
1518 }
1519
1520 // ---- If Track Finished ----
1521 if (trackStarted && digitalRead(MP3_BUSY_PIN) == HIGH) {
1522 trackStarted = false;
1523 subStep++;
1524 delay(300);
1525 return;
1526 }
1527
1528 // ---- Manual Skip ----
1529 if (digitalRead(BTN_SET) == LOW) {
1530 mp3.stop();
1531 delay(300);
1532 trackStarted = false;
1533 subStep++;
1534 return;
1535 }
1536
1537 return;
1538 }
1539
1540 step = 8;
1541 return;
1542
1543 case 8: // 6. Button Test Setup
1544 lcd.clear();
1545 lcdPrintRow(0, "6. BUTTON TEST");
1546 subStep = 1;
1547 step = 9;
1548 return;
1549
1550 case 9: // 6. Button Execution (Wait for Release)
1551 if (subStep == 1) {
1552 lcdPrintRow(1, "PRESS: SET (B14)");
1553 if (digitalRead(BTN_SET) == LOW) {
1554 lcdPrintRow(2, "SET OK!");
1555 while(digitalRead(BTN_SET) == LOW); // WAIT FOR RELEASE
1556 delay(500);
1557 subStep++;
1558 }
1559 } else if (subStep == 2) {
1560 lcdPrintRow(1, "PRESS: UP (B13)");
1561 if (digitalRead(BTN_UP) == LOW) {
1562 lcdPrintRow(2, "UP OK!");
1563 while(digitalRead(BTN_UP) == LOW); // WAIT FOR RELEASE
1564 delay(500);
1565 subStep++;
1566 }
1567 } else if (subStep == 3) {
1568 lcdPrintRow(1, "PRESS: DOWN(B12)");
1569 if (digitalRead(BTN_DOWN) == LOW) {
1570 lcdPrintRow(2, "DOWN OK!");
1571 while(digitalRead(BTN_DOWN) == LOW); // WAIT FOR RELEASE
1572 delay(500);
1573 subStep++;
1574 }
1575 } else {
1576 step = 10;
1577 }
1578 return;
1579
1580 case 10: // Done
1581 lcd.clear();
1582 lcdPrintRow(1, "TEST COMPLETED");
1583 lcdPrintRow(3, "PRESS SET TO EXIT");
1584 return;
1585 }
1586
1587 if (step < 6) step++;
1588}
1589
1590///////////////////////////////////////////////////////
1591////////////////////////////////////////////////////
1592uint32_t rtcToSeconds()
1593{
1594 return (uint32_t)rtc.getHours() * 3600UL
1595 + (uint32_t)rtc.getMinutes() * 60UL
1596 + (uint32_t)rtc.getSeconds();
1597}
1598
1599
1600
1601/////////////////////////////////////////////////
1602bool bothPressed(uint8_t p1, uint8_t p2)
1603{
1604 static unsigned long t = 0;
1605
1606 if (digitalRead(p1) == LOW && digitalRead(p2) == LOW)
1607 {
1608 if (millis() - t > 400) // hold both for 400ms
1609 return true;
1610 }
1611 else
1612 {
1613 t = millis();
1614 }
1615
1616 return false;
1617}
1618
1619///////////////////////////////////////////////////
1620///////////////////////////////////////////////////
1621void updateExamMatrixPattern(uint32_t left, uint32_t total) {
1622 matrix.clearDisplay(0);
1623
1624 // ਸਮੇਂ ਦੇ ਹਿਸਾਬ ਨਾਲ ਉਚਾਈ (0 ਤੋਂ 8) ਕੱਢੋ
1625 int height = map(left, 0, total, 0, 8);
1626
1627 // 8 ਵਰਟੀਕਲ ਬਾਰਾਂ ਬਣਾਉਣ ਲਈ ਲੂਪ
1628 for (int col = 0; col < 8; col++) {
1629 for (int row = 0; row < height; row++) {
1630 // ਹੇਠਾਂ ਤੋਂ ਉੱਪਰ ਵੱਲ ਬਾਰਾਂ ਭਰੋ
1631 matrix.setLed(0, 7 - row, col, true);
1632 }
1633 }
1634}
1635
1636void screenExamTimer() {
1637 static uint8_t step = 0;
1638 static uint8_t lastStep = 255;
1639 static unsigned long lastMatrixUpdate = 0;
1640
1641 // --- 1. LCD ਦੀ ਪਹਿਲੀ ਲਾਈਨ (ਹਮੇਸ਼ਾ ਚੱਲਦੀ ਘੜੀ) ---
1642 char clockLine[21];
1643 snprintf(clockLine, sizeof(clockLine), "%02d/%02d %02d:%02d:%02d",
1644 rtc.getDay(), rtc.getMonth(), rtc.getHours(), rtc.getMinutes(), rtc.getSeconds());
1645 lcd.setCursor(0, 0);
1646 lcd.print(clockLine);
1647
1648 if (screenChanged) {
1649 if (!examScheduled && !examRunning) {
1650 examStartH = rtc.getHours();
1651 examStartM = rtc.getMinutes();
1652 examDurH = 3;
1653 examDurM = 0;
1654 step = 0;
1655 }
1656 lastStep = 255;
1657 lcd.clear();
1658 screenChanged = false;
1659 }
1660
1661 /* ================= WAITING (ਸਮਾਂ ਹੋਣ ਦੀ ਉਡੀਕ) ================= */
1662 if (examScheduled && !examRunning) {
1663 uint32_t now = rtcToSeconds();
1664 uint32_t startSec = (uint32_t)examStartH * 3600UL + (uint32_t)examStartM * 60UL;
1665
1666 lcdPrintRow(1, "STATUS: WAITING...");
1667 char startLine[21];
1668 snprintf(startLine, sizeof(startLine), "START AT: %02d:%02d", examStartH, examStartM);
1669 lcdPrintRow(2, startLine);
1670 lcdPrintRow(3, "UP+SET = CANCEL");
1671
1672 // ਸਿਰਫ਼ ਉਸੇ ਸੈਕਿੰਡ 'ਤੇ ਸਟਾਰਟ ਹੋਵੇਗਾ ਜਦੋਂ ਸਮਾਂ ਮੈਚ ਕਰੇਗਾ
1673 if (now == startSec && rtc.getSeconds() == 0) {
1674 examScheduled = false;
1675 examRunning = true;
1676 examStartSec = now;
1677
1678 digitalWrite(RELAY_PIN, LOW); // ਘੰਟੀ ਚਾਲੂ
1679 digitalWrite(BUZZER_PIN, HIGH);
1680 mp3.playMp3Folder(15); // Exam Start Punjabi Audio
1681 delay(2000); // ਸਿਰਫ਼ ਸਟਾਰਟ ਅਲਰਟ ਲਈ ਛੋਟਾ ਡਿਲੇਅ
1682 digitalWrite(RELAY_PIN, HIGH); // ਘੰਟੀ ਬੰਦ
1683 digitalWrite(BUZZER_PIN, LOW);
1684 lcd.clear();
1685 }
1686
1687 if (bothPressed(BTN_UP, BTN_SET)) {
1688 examScheduled = false;
1689 uiState = UI_MENU;
1690 screenChanged = true;
1691 }
1692 return;
1693 }
1694
1695 /* ================= RUNNING (ਪ੍ਰੀਖਿਆ ਚੱਲ ਰਹੀ ਹੈ) ================= */
1696 if (examRunning) {
1697 uint32_t now = rtcToSeconds();
1698 uint32_t elapsed = now - examStartSec;
1699
1700 if (elapsed >= examDurationSec) {
1701 examRunning = false;
1702 digitalWrite(RELAY_PIN, LOW);
1703 digitalWrite(BUZZER_PIN, HIGH);
1704 mp3.playMp3Folder(16); // Exam Over Punjabi Audio
1705 delay(3000);
1706 digitalWrite(RELAY_PIN, HIGH);
1707 digitalWrite(BUZZER_PIN, LOW);
1708 lcd.clear();
1709 showError("EXAM FINISHED", "TIME UP");
1710 uiState = UI_DASHBOARD;
1711 screenChanged = true;
1712 return;
1713 }
1714
1715 // 8x8 ਮੈਟ੍ਰਿਕਸ ਪੈਟਰਨ ਅੱਪਡੇਟ (ਹਰ 1 ਸੈਕਿੰਡ ਬਾਅਦ)
1716 if (millis() - lastMatrixUpdate > 1000) {
1717 updateExamMatrixPattern(examDurationSec - elapsed, examDurationSec);
1718 lastMatrixUpdate = millis();
1719 }
1720
1721 uint32_t left = examDurationSec - elapsed;
1722 char leftLine[21];
1723 lcdPrintRow(1, "EXAM IN PROGRESS");
1724 snprintf(leftLine, sizeof(leftLine), "TIME LEFT: %02lu:%02lu:%02lu", left/3600, (left%3600)/60, left%60);
1725 lcdPrintRow(2, leftLine);
1726 lcdPrintRow(3, "UP+SET = STOP");
1727
1728 if (bothPressed(BTN_UP, BTN_SET)) {
1729 examRunning = false;
1730 matrix.clearDisplay(0);
1731 uiState = UI_MENU;
1732 screenChanged = true;
1733 }
1734 return;
1735 }
1736
1737 /* ================= EDIT / SETUP (ਸੈਟਿੰਗ ਮੋਡ) ================= */
1738 if (step != lastStep) {
1739 if (step < 4) lcdPrintRow(3, "SET=NEXT");
1740 else lcdPrintRow(3, "SET=START UP=BACK");
1741 lastStep = step;
1742 }
1743
1744 char sLine[21], dLine[21];
1745 snprintf(sLine, sizeof(sLine), "SET START: %02d:%02d", examStartH, examStartM);
1746 lcdPrintRow(1, sLine);
1747 snprintf(dLine, sizeof(dLine), "SET DUR : %02d:%02d", examDurH, examDurM);
1748 lcdPrintRow(2, dLine);
1749
1750 lcd.blink();
1751 if (step == 0) lcd.setCursor(11, 1);
1752 else if (step == 1) lcd.setCursor(14, 1);
1753 else if (step == 2) lcd.setCursor(11, 2);
1754 else if (step == 3) lcd.setCursor(14, 2);
1755 else lcd.noBlink();
1756
1757 if (readButton(BTN_UP, btnUp)) {
1758 if (step == 0) examStartH = (examStartH + 1) % 24;
1759 else if (step == 1) examStartM = (examStartM + 1) % 60;
1760 else if (step == 2) examDurH = (examDurH + 1) % 24;
1761 else if (step == 3) examDurM = (examDurM + 1) % 60;
1762 }
1763 if (readButton(BTN_DOWN, btnDown)) {
1764 if (step == 0) examStartH = (examStartH + 23) % 24;
1765 else if (step == 1) examStartM = (examStartM + 59) % 60;
1766 else if (step == 2) examDurH = (examDurH + 23) % 24;
1767 else if (step == 3) examDurM = (examDurM + 59) % 60;
1768 }
1769
1770 if (readButton(BTN_SET, btnSet)) {
1771 if (step < 4) { step++; return; }
1772 examDurationSec = (uint32_t)examDurH * 3600UL + (uint32_t)examDurM * 60UL;
1773 examScheduled = true;
1774 step = 0;
1775 lcd.clear();
1776 lcd.noBlink();
1777 }
1778}
1779///////////////////////////////////////////////////
1780///////////////////////////////////////////////////
1781
1782//////////////////////////////////////////////////
1783
1784
1785
1786///////////////////////////////////////////////////
1787//
1788void drawTwoDigitScroll(uint8_t d1, uint8_t d2)
1789{
1790 static uint8_t offset = 0;
1791 static unsigned long t = 0;
1792
1793 if (millis() - t > 120) // scroll speed
1794 {
1795 t = millis();
1796 offset++;
1797 if (offset > 8) offset = 0;
1798 }
1799
1800 matrix.clearDisplay(0);
1801
1802 for (int row = 0; row < 8; row++)
1803 {
1804 // your drawChar8x8 uses flipped access
1805 byte a = font8x8[d1][7 - row];
1806 byte b = font8x8[d2][7 - row];
1807
1808 // combine two chars side by side (16 bit wide)
1809 uint16_t line16 = ((uint16_t)a << 8) | b;
1810
1811 // take sliding 8-bit window
1812 byte out = (line16 >> (8 - offset)) & 0xFF;
1813
1814 for (int col = 0; col < 8; col++)
1815 {
1816 bool on = out & (1 << col); // keep your horizontal flip
1817 matrix.setLed(0, row, col, on);
1818 }
1819 }
1820}
1821
1822//////////////////////////////////////////////////////
1823bool generateAutoTimetable(
1824 uint8_t startH,
1825 uint8_t startM,
1826
1827 uint8_t prayerMinutes, // FIRST activity
1828 uint8_t periodMinutes,
1829 uint8_t totalPeriods,
1830
1831 uint8_t recessMinutes,
1832 uint8_t recessAfterPeriod // 1..totalPeriods , 0 = no recess
1833)
1834{
1835 bellCount = 0;
1836
1837 int cur = startH * 60 + startM;
1838
1839 /* ---------- FIRST : PRAYER ---------- */
1840 if (prayerMinutes > 0)
1841 {
1842 if (bellCount >= MAX_BELLS) return false;
1843
1844 bellHour[bellCount] = cur / 60;
1845 bellMin [bellCount] = cur % 60;
1846 bellType[bellCount] = BELL_PRAYER;
1847 bellCount++;
1848
1849 cur += prayerMinutes;
1850 }
1851
1852 /* ---------- PERIODS ---------- */
1853 for (uint8_t p = 1; p <= totalPeriods; p++)
1854 {
1855 if (bellCount >= MAX_BELLS) return false;
1856
1857 // period start bell
1858 bellHour[bellCount] = cur / 60;
1859 bellMin [bellCount] = cur % 60;
1860 bellType[bellCount] = BELL_PERIOD;
1861 bellCount++;
1862
1863 cur += periodMinutes;
1864
1865 // recess after this period
1866 if (recessAfterPeriod == p && recessMinutes > 0)
1867 {
1868 if (bellCount >= MAX_BELLS) return false;
1869
1870 bellHour[bellCount] = cur / 60;
1871 bellMin [bellCount] = cur % 60;
1872 bellType[bellCount] = BELL_RECESS;
1873 bellCount++;
1874
1875 cur += recessMinutes;
1876 }
1877 }
1878
1879 /* ---------- END ---------- */
1880 if (bellCount >= MAX_BELLS) return false;
1881
1882 bellHour[bellCount] = cur / 60;
1883 bellMin [bellCount] = cur % 60;
1884 bellType[bellCount] = BELL_END;
1885 bellCount++;
1886
1887 return true;
1888}
1889/////////////////////////////////////////
1890void screenAutoGenerateBells()
1891{
1892 static uint8_t step = 0;
1893 static uint8_t mode = 0; // 0 = input, 1 = preview, 2 = confirm
1894
1895 static uint8_t startH, startM;
1896 static uint8_t prayerMin;
1897 static uint8_t periodMin;
1898 static uint8_t totalPeriods;
1899 static uint8_t recessMin;
1900 static uint8_t recessAfter;
1901
1902 static uint8_t top = 0; // for preview scroll
1903
1904 if (screenChanged)
1905 {
1906 startH = 8;
1907 startM = 0;
1908
1909 prayerMin = 10;
1910 periodMin = 40;
1911 totalPeriods = 6;
1912 recessMin = 20;
1913 recessAfter = 3;
1914
1915 step = 0;
1916 mode = 0;
1917 top = 0;
1918
1919 lcd.clear();
1920 lcd.blink();
1921 screenChanged = false;
1922 }
1923
1924 /* =====================================================
1925 MODE 1 : PREVIEW GENERATED BELLS (RAM ONLY)
1926 ===================================================== */
1927 if (mode == 1)
1928 {
1929 lcd.noBlink();
1930
1931 lcdPrintRow(0, "AUTO PREVIEW");
1932
1933 for (uint8_t r = 0; r < 2; r++)
1934 {
1935 uint8_t i = top + r;
1936
1937 if (i < bellCount)
1938 {
1939 char line[21];
1940 char c;
1941
1942 if (bellType[i] == BELL_PERIOD) c = 'C';
1943 else if (bellType[i] == BELL_PRAYER) c = 'P';
1944 else if (bellType[i] == BELL_RECESS) c = 'R';
1945 else if (bellType[i] == BELL_END) c = 'E';
1946 else c = '?';
1947
1948 snprintf(line, sizeof(line),
1949 "B%02d %02d:%02d %c",
1950 i + 1,
1951 bellHour[i],
1952 bellMin[i],
1953 c);
1954
1955 lcdPrintRow(r + 1, line);
1956 }
1957 else
1958 lcdPrintRow(r + 1, "");
1959 }
1960
1961 lcdPrintRow(3, "UP/DN SCROLL SET");
1962
1963 if (readButton(BTN_DOWN, btnDown))
1964 if (top + 2 < bellCount) top++;
1965
1966 if (readButton(BTN_UP, btnUp))
1967 if (top > 0) top--;
1968
1969 if (readButton(BTN_SET, btnSet))
1970 {
1971 mode = 2; // go to save confirm
1972 lcd.clear();
1973 }
1974
1975 return;
1976 }
1977
1978 /* =====================================================
1979 MODE 2 : SAVE CONFIRM
1980 ===================================================== */
1981 if (mode == 2)
1982 {
1983 lcd.noBlink();
1984
1985 lcdPrintRow(0, "SAVE TIMETABLE?");
1986 lcdPrintRow(1, "SET=YES UP=NO");
1987 lcdPrintRow(2, "");
1988 lcdPrintRow(3, "");
1989
1990 if (readButton(BTN_SET, btnSet))
1991 {
1992 char name[TT_NAME_LEN + 1];
1993 makeAutoTimetableName(name);
1994
1995 saveCurrentTimetable(name);
1996
1997 showError("AUTO TIME", "SAVED");
1998
1999 uiState = UI_MENU;
2000 screenChanged = true;
2001 return;
2002 }
2003
2004 if (readButton(BTN_UP, btnUp))
2005 {
2006 mode = 1; // back to preview
2007 lcd.clear();
2008 return;
2009 }
2010
2011 return;
2012 }
2013
2014
2015
2016 /* =====================================================
2017 MODE 0 : INPUT SCREEN
2018 ===================================================== */
2019
2020/* =====================================================
2021 MODE 0 : INPUT SCREEN
2022 ===================================================== */
2023
2024 lcdPrintRow(0, "AUTO GENERATE");
2025
2026 char line[21];
2027
2028 switch (step)
2029 {
2030 case 0:
2031 snprintf(line, sizeof(line), "START %02d:%02d", startH, startM);
2032 lcdPrintRow(1, line);
2033 lcd.setCursor(6,1);
2034 break;
2035
2036 case 1:
2037 snprintf(line, sizeof(line), "START %02d:%02d", startH, startM);
2038 lcdPrintRow(1, line);
2039 lcd.setCursor(9,1);
2040 break;
2041
2042 case 2:
2043 snprintf(line, sizeof(line), "PRAYER %02d MIN", prayerMin);
2044 lcdPrintRow(1, line);
2045 lcd.setCursor(7,1);
2046 break;
2047
2048 case 3:
2049 snprintf(line, sizeof(line), "PERIOD %02d MIN", periodMin);
2050 lcdPrintRow(1, line);
2051 lcd.setCursor(7,1);
2052 break;
2053
2054 case 4:
2055 snprintf(line, sizeof(line), "TOTAL P %02d", totalPeriods);
2056 lcdPrintRow(1, line);
2057 lcd.setCursor(8,1);
2058 break;
2059
2060 case 5:
2061 snprintf(line, sizeof(line), "RECESS %02d MIN", recessMin);
2062 lcdPrintRow(1, line);
2063 lcd.setCursor(7,1);
2064 break;
2065
2066 case 6:
2067 snprintf(line, sizeof(line), "RECESS AFT %02d", recessAfter);
2068 lcdPrintRow(1, line);
2069 lcd.setCursor(11,1);
2070 break;
2071 }
2072
2073 lcdPrintRow(2, "");
2074 lcdPrintRow(3, "UP/DN SET=NEXT");
2075 lcd.blink();
2076
2077 /* -------- edit values -------- */
2078
2079 if (readButton(BTN_UP, btnUp))
2080 {
2081 if (step == 0 && startH < 23) startH++;
2082 else if (step == 1 && startM < 59) startM++;
2083 else if (step == 2 && prayerMin < 60) prayerMin++;
2084 else if (step == 3 && periodMin < 90) periodMin++;
2085 else if (step == 4 && totalPeriods < 17) totalPeriods++;
2086 else if (step == 5 && recessMin < 60) recessMin++;
2087 else if (step == 6 && recessAfter < totalPeriods) recessAfter++;
2088 }
2089
2090 if (readButton(BTN_DOWN, btnDown))
2091 {
2092 if (step == 0 && startH > 0) startH--;
2093 else if (step == 1 && startM > 0) startM--;
2094 else if (step == 2 && prayerMin > 0) prayerMin--;
2095 else if (step == 3 && periodMin > 1) periodMin--;
2096 else if (step == 4 && totalPeriods > 1) totalPeriods--;
2097 else if (step == 5 && recessMin > 0) recessMin--;
2098 else if (step == 6 && recessAfter > 0) recessAfter--;
2099 }
2100 if (recessAfter > totalPeriods)
2101 recessAfter = totalPeriods;
2102 /* -------- SET -------- */
2103
2104 if (readButton(BTN_SET, btnSet))
2105 {
2106 if (step < 6)
2107 {
2108 step++;
2109 return;
2110 }
2111
2112 // last field confirmed → generate now
2113
2114 if (!generateAutoTimetable(
2115 startH, startM,
2116 prayerMin,
2117 periodMin,
2118 totalPeriods,
2119 recessMin,
2120 recessAfter))
2121 {
2122 showError("FAILED", "TOO MANY");
2123 screenChanged = true;
2124 return;
2125 }
2126
2127 // go to preview
2128 top = 0;
2129 mode = 1;
2130 lcd.clear();
2131 return;
2132 }
2133}
2134
2135
2136/* ================= SETUP ================= */
2137/* ================= SETUP ================= */
2138/* ================= SETUP ================= */
2139/* ================= SETUP ================= */
2140void screenTriggerBell() {
2141 static bool triggering = false;
2142 static unsigned long lastFlash = 0;
2143 static bool flashState = false;
2144
2145 if (screenChanged) {
2146 lcd.clear();
2147 triggering = false;
2148 screenChanged = false;
2149 }
2150
2151 lcdPrintRow(0, "MANUAL TRIGGER");
2152
2153 if (!triggering) {
2154 lcdPrintRow(1, "READY TO RING");
2155 lcdPrintRow(3, "SET=START UP=BACK");
2156
2157 if (readButton(BTN_SET, btnSet)) {
2158 mp3.stop();
2159 delay(50);
2160 mp3.playMp3Folder(17); // Play Emergency Punjabi Script
2161 delay(200); // Wait for DFPlayer to pull Busy Pin LOW
2162
2163 digitalWrite(RELAY_PIN, LOW); // Industrial Bell ON
2164 digitalWrite(BUZZER_PIN, HIGH); // Internal Buzzer ON
2165
2166 triggering = true;
2167 lcd.clear();
2168 }
2169 }
2170 else {
2171 lcdPrintRow(1, "!! EMERGENCY !!");
2172 lcdPrintRow(2, " ANNOUNCING ");
2173
2174 // --- 8x8 Matrix Flashing Logic ---
2175 if (millis() - lastFlash > 300) { // Flash every 300ms
2176 lastFlash = millis();
2177 flashState = !flashState;
2178 if (flashState) drawChar8x8(10); // Show 'C'
2179 else matrix.clearDisplay(0); // Turn off
2180 }
2181
2182 // --- Check if MP3 has finished ---
2183 // digitalRead(MP3_BUSY_PIN) == HIGH means the sound has stopped
2184 if (digitalRead(MP3_BUSY_PIN) == HIGH) {
2185 digitalWrite(RELAY_PIN, HIGH); // Bell OFF
2186 digitalWrite(BUZZER_PIN, LOW); // Buzzer OFF
2187
2188 triggering = false;
2189 matrix.clearDisplay(0);
2190 showError("ALERT FINISHED", "");
2191 uiState = UI_MENU;
2192 screenChanged = true;
2193 }
2194 }
2195}
2196/* ================= SETUP ================= */
2197void screenTriggerBell1() {
2198 static bool triggering = false;
2199 static unsigned long startTime = 0;
2200
2201 if (screenChanged) {
2202 lcd.clear();
2203 lcd.noBlink();
2204 triggering = false;
2205 screenChanged = false;
2206 }
2207
2208 lcdPrintRow(0, "MANUAL TRIGGER");
2209
2210 if (!triggering) {
2211 lcdPrintRow(1, "READY TO RING");
2212 lcdPrintRow(3, "SET=START UP=BACK");
2213
2214 if (readButton(BTN_UP, btnUp)) {
2215 uiState = UI_MENU;
2216 screenChanged = true;
2217 }
2218
2219 if (readButton(BTN_SET, btnSet)) {
2220 // Start the physical bell
2221 digitalWrite(RELAY_PIN, LOW);
2222 digitalWrite(BUZZER_PIN, HIGH);
2223
2224 // Start the MP3 (Track 1 is usually the standard bell)
2225 //mp3.play(1);
2226 mp3.play(17);
2227 //mp3.playMp3Folder(17);
2228 delay(100); // Small pause for serial to finish
2229 // Step 2: Show the Matrix icon
2230 drawChar8x8(10);
2231 delay(100);
2232 startTime = millis();
2233 triggering = true;
2234 lcd.clear();
2235 }
2236 }
2237 else {
2238 // While the bell is ringing
2239 lcdPrintRow(1, "RINGING...");
2240 lcdPrintRow(2, "PLEASE WAIT");
2241
2242 // Show an animation on the Matrix to indicate action
2243 drawChar8x8(10); // Show 'C' or a custom bell icon
2244
2245 // Stop after 3 seconds
2246 if (millis() - startTime >= 3000) {
2247 digitalWrite(RELAY_PIN, HIGH);
2248 digitalWrite(BUZZER_PIN, LOW);
2249
2250 triggering = false;
2251 matrix.clearDisplay(0);
2252 showError("BELL FINISHED", "");
2253 uiState = UI_MENU;
2254 screenChanged = true;
2255 }
2256 }
2257}
2258/* ================= SETUP ================= */
2259void screenClearEEPROM() {
2260 static bool confirm = false;
2261
2262 if (screenChanged) {
2263 confirm = false;
2264 lcd.clear();
2265 lcd.noBlink();
2266 screenChanged = false;
2267 }
2268
2269 if (!confirm) {
2270 lcdPrintRow(0, "!!! WARNING !!!");
2271 lcdPrintRow(1, "WIPE ALL DATA?");
2272 lcdPrintRow(3, "SET=YES UP=BACK");
2273
2274 if (readButton(BTN_UP, btnUp)) {
2275 uiState = UI_MENU;
2276 screenChanged = true;
2277 }
2278
2279 if (readButton(BTN_SET, btnSet)) {
2280 confirm = true; // Move to the actual clearing phase
2281 lcd.clear();
2282 }
2283 }
2284 else {
2285 lcdPrintRow(1, "CLEARING...");
2286
2287 // 1. Reset the Timetable Count (Bytes 0 & 1)
2288 setTimetableCount(0);
2289
2290 // 2. Reset Default Index (Byte 2)
2291 setDefaultTimetable(0);
2292
2293 // 3. Clear the first 32 bytes (Management Area + First Name)
2294 // This ensures even if the count was wrong, the system sees 0s
2295 for (uint16_t i = 0; i < 32; i++) {
2296 eepromWriteByte(i, 0);
2297
2298 // Visual feedback on the Matrix while it's working
2299 if (i % 4 == 0) matrix.setLed(0, 0, i/4, true);
2300 }
2301
2302 // Reset RAM variables
2303 bellCount = 0;
2304
2305 showError("EEPROM WIPED", "SYSTEM RESET");
2306 matrix.clearDisplay(0);
2307 uiState = UI_DASHBOARD;
2308 screenChanged = true;
2309 }
2310}
2311/* ================= SETUP ================= */
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348/* ================= SETUP ================= */
2349/* ================= SETUP ================= */
2350
2351/* ================= SETUP ================= */
2352void setup() {
2353
2354 pinMode(BTN_SET, INPUT_PULLUP);
2355 pinMode(BTN_UP, INPUT_PULLUP);
2356 pinMode(BTN_DOWN, INPUT_PULLUP);
2357
2358 pinMode(BUZZER_PIN, OUTPUT);
2359 pinMode(RELAY_PIN, OUTPUT);
2360
2361 pinMode(MP3_BUSY_PIN, INPUT_PULLUP);
2362
2363 digitalWrite(BUZZER_PIN, LOW);
2364 digitalWrite(RELAY_PIN, HIGH);
2365
2366 Wire.begin();
2367 Wire.setClock(100000);
2368 Wire.setTimeout(20);
2369
2370uint16_t cnt = getTimetableCount();
2371if (cnt > 50)
2372{
2373 setTimetableCount(0);
2374 setDefaultTimetable(0);
2375}
2376
2377
2378 lcd.init();
2379 lcd.backlight();
2380
2381 // scanI2C();
2382
2383 rtc.setClockSource(STM32RTC::LSE_CLOCK);
2384 rtc.begin();
2385
2386// loadBellsFromEEPROM();
2387uint16_t n = getTimetableCount();
2388
2389if (n > 0)
2390{
2391 uint8_t d = getDefaultTimetable();
2392 if (d >= n) d = 0;
2393
2394 loadTimetable(d);
2395}
2396 // default
2397else
2398 bellCount = 0;
2399
2400
2401
2402
2403 pinMode(MATRIX_CS, OUTPUT);
2404 pinMode(MATRIX_CLK, OUTPUT);
2405 pinMode(MATRIX_DIN, OUTPUT);
2406
2407 matrix.shutdown(0, false);
2408 matrix.setIntensity(0, 5);
2409 matrix.clearDisplay(0);
2410
2411 /* ---- DFPLAYER ---- */
2412 mp3Serial.begin(9600);
2413 mp3.begin(mp3Serial);
2414 mp3.volume(25);
2415
2416 lcd.clear();
2417 uiState = UI_DASHBOARD;
2418 screenChanged = true;
2419}
2420
2421/* ================= LOOP ================= */
2422void loop()
2423{
2424 int cb, nb;
2425 long cd;
2426
2427 getBellStatus(cb, nb, cd);
2428
2429 /* =====================================================
2430 1️⃣ EXACT TIME TRIGGER (ONLY ONCE PER MINUTE)
2431 ===================================================== */
2432
2433 static int lastBellMinute = -1;
2434
2435 int nowH = rtc.getHours();
2436 int nowM = rtc.getMinutes();
2437 int nowS = rtc.getSeconds();
2438
2439 int currentMinute = nowH * 60 + nowM;
2440
2441 if (!examRunning)
2442 {
2443 // Trigger only at second 0 and only once per minute
2444 if (nowS == 0 && currentMinute != lastBellMinute)
2445 {
2446 for (int i = 0; i < bellCount; i++)
2447 {
2448 if (bellHour[i] == nowH &&
2449 bellMin[i] == nowM)
2450 {
2451 activeBellIndex = i;
2452 bellActive = true;
2453 mp3Playing = true;
2454
2455 digitalWrite(BUZZER_PIN, HIGH);
2456 digitalWrite(RELAY_PIN, LOW);
2457
2458 break;
2459 }
2460 }
2461
2462 lastBellMinute = currentMinute;
2463 }
2464 }
2465
2466 /* =====================================================
2467 2️⃣ MATRIX DISPLAY
2468 ===================================================== */
2469/* -------- MATRIX DISPLAY -------- */
2470
2471if (!examRunning)
2472{
2473 // Show current bell if valid
2474 if (cb >= 0 && !(cb == bellCount - 1 && nb == -1 && !bellActive))
2475 {
2476 uint8_t t = bellType[cb];
2477
2478 // 🔹 PRAYER
2479 if (t == BELL_PRAYER)
2480 {
2481 drawChar8x8(12); // P
2482 }
2483
2484 // 🔹 RECESS
2485 else if (t == BELL_RECESS)
2486 {
2487 drawChar8x8(11); // R
2488 }
2489
2490 // 🔹 END
2491 else if (t == BELL_END)
2492 {
2493 // Show only while ringing
2494 if (bellActive)
2495 drawChar8x8(13); // E
2496 else
2497 matrix.clearDisplay(0);
2498 }
2499
2500 // 🔹 PERIOD
2501 else if (t == BELL_PERIOD)
2502 {
2503 int periodNumber = 0;
2504
2505 for (int i = 0; i <= cb; i++)
2506 if (bellType[i] == BELL_PERIOD)
2507 periodNumber++;
2508
2509 if (periodNumber < 10)
2510 drawChar8x8(periodNumber);
2511 else
2512 drawTwoDigitScroll(periodNumber / 10,
2513 periodNumber % 10);
2514 }
2515 }
2516 else
2517 {
2518 // Show countdown when no active bell
2519 updateCountdownIndicators(cd);
2520 }
2521}
2522else
2523{
2524 // -------- EXAM MODE MATRIX --------
2525 uint32_t now = rtcToSeconds();
2526
2527 if (now >= examStartSec)
2528 {
2529 uint32_t elapsed = now - examStartSec;
2530
2531 if (elapsed < examDurationSec)
2532 showCountdownOnMatrix(examDurationSec - elapsed);
2533 else
2534 matrix.clearDisplay(0);
2535 }
2536 else
2537 {
2538 matrix.clearDisplay(0);
2539 }
2540}
2541
2542
2543
2544
2545
2546 /* =====================================================
2547 3️⃣ BELL DURATION HANDLER
2548 ===================================================== */
2549
2550 if (!examRunning)
2551 handleBell();
2552
2553 /* =====================================================
2554 4️⃣ MP3 CONTROL
2555 ===================================================== */
2556
2557/* -------- MP3 CONTROL -------- */
2558
2559if (mp3Playing)
2560{
2561 if (activeBellIndex >= 0)
2562 {
2563 uint8_t t = bellType[activeBellIndex];
2564 uint16_t track = 0;
2565
2566 // 🔹 PRAYER
2567 if (t == BELL_PRAYER)
2568 {
2569 track = 11; // 0011.mp3
2570 }
2571
2572 // 🔹 RECESS
2573 else if (t == BELL_RECESS)
2574 {
2575 track = 12; // 0012.mp3
2576 }
2577
2578 // 🔹 END
2579 else if (t == BELL_END)
2580 {
2581 track = 13; // 0013.mp3
2582 }
2583
2584 // 🔹 PERIOD
2585 else if (t == BELL_PERIOD)
2586 {
2587 int periodNumber = 0;
2588
2589 // Count only period bells up to this index
2590 for (int i = 0; i <= activeBellIndex; i++)
2591 {
2592 if (bellType[i] == BELL_PERIOD)
2593 periodNumber++;
2594 }
2595
2596 // Safety limit (1 to 10)
2597 if (periodNumber >= 1 && periodNumber <= 10)
2598 track = periodNumber;
2599 }
2600
2601 // 🔥 Play only if track valid
2602 if (track > 0)
2603 {
2604 mp3.play(track);
2605
2606 }
2607 }
2608
2609 mp3Playing = false;
2610}
2611
2612
2613 /* =====================================================
2614 5️⃣ UI HANDLER
2615 ===================================================== */
2616
2617 switch (uiState)
2618 {
2619 case UI_DASHBOARD: screenDashboard(); break;
2620 case UI_MENU: screenMenu(); break;
2621 case UI_SET_DATETIME: screenSetDateTime(); break;
2622 case UI_SET_BELL: screenSetBell(); break;
2623 case UI_MANAGE_Timetable: screenTimetableList(); break;
2624 case UI_CLEAR_EEPROM: screenClearEEPROM(); break;
2625 case UI_TRIGGER_BELL: screenTriggerBell(); break;
2626 case UI_EXAM_TIMER: screenExamTimer(); break;
2627 case UI_AUTO_BELLS: screenAutoGenerateBells(); break;
2628 case UI_SELFTEST: screenSelfTest(); break;
2629 }
2630
2631 delay(80);
2632}
2633
2634/* ================= LOOP ================= */




Note: Content and images are from: https://projecthub.arduino.cc/, with some modifications.
If you want it removed due to copyright reasons, please leave a comment. Thank you.
I want to share this article more widely so that everyone knows about Arduino and your project.

Easy remote control

Remotely control an LED with ease. Devices and components Arduino Nano 33 BLE with headers Box 525 Resistors precision 1% - 17 values Breadb...