#include <Arduino.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
// ============================================================================
// 01. KHAI BAO PHAN CUNG - DOI CHAN / DIA CHI LCD TAI DAY
// ============================================================================
const uint8_t LCD_ADDRESS = 0x27;
const uint8_t LCD_COLS = 20;
const uint8_t LCD_ROWS = 4;
LiquidCrystal_I2C lcd(LCD_ADDRESS, LCD_COLS, LCD_ROWS);
const uint8_t RED13 = 12;
const uint8_t YELLOW13 = 11;
const uint8_t GREEN13 = 10;
const uint8_t RED24 = 7;
const uint8_t YELLOW24 = 6;
const uint8_t GREEN24 = 5;
const uint8_t LIGHT_PINS[6] = {RED13, YELLOW13, GREEN13,
RED24, YELLOW24, GREEN24};
// LED 7 doan 2 chu so 7SEG-MPX2-CC theo so do Proteus moi.
// LED24: A..G = D22..D28, chan chon so 1/2 = D29/D30.
// LED13: A..G = D31..D37, chan chon so 1/2 = D38/D39.
const uint8_t SEG13[7] = {22, 23, 24, 25, 26, 27, 28}; // A,B,C,D,E,F,G
const uint8_t DIGIT13[2] = {36, 37}; // 1,2
const uint8_t SEG24[7] = {29, 30, 31, 32, 33, 34, 35}; // A,B,C,D,E,F,G
const uint8_t DIGIT24[2] = {38, 39}; // 1,2
const bool SEG_COMMON_ANODE = false; // 7SEG-MPX2-CC = chung am.
const uint8_t BTN_MENU = A0;
const uint8_t BTN_UP = A1;
const uint8_t BTN_DOWN = A2;
const uint8_t BTN_SAVE = A3; // Moi nut: 1 dau vao chan, 1 dau xuong GND.
const uint16_t MAX_RED_SECONDS = 99;
const uint32_t DEBOUNCE_MS = 30;
const uint32_t DOUBLE_CLICK_MS = 450;
const uint32_t REPEAT_START_MS = 600;
const uint32_t REPEAT_INTERVAL_MS = 120;
const uint32_t NIGHT_BLINK_MS = 500;
const uint32_t START_RED_MS = 1000; // Cung do khi khoi dong / tu dem sang ngay.
const uint32_t LCD_REFRESH_MS = 150;
const uint32_t SECONDS_PER_DAY = 86400UL;
// ============================================================================
// 02. KIEU DU LIEU VA BIEN TRANG THAI
// ============================================================================
struct __attribute__((packed)) Settings {
uint8_t green13;
uint8_t yellow13;
uint8_t green24;
uint8_t yellow24;
uint16_t nightStart; // So phut tinh tu 00:00, 0..1439.
uint16_t dayStart;
uint8_t nightEnabled;
};
struct __attribute__((packed)) EepromRecord {
uint16_t magic;
uint8_t version;
uint32_t sequence;
Settings data;
uint16_t crc;
uint8_t committed; // Ghi cuoi cung, sau du lieu va CRC.
};
struct Button {
uint8_t pin;
bool rawDown;
bool down;
bool pressed;
uint32_t changedAt;
uint32_t pressedAt;
uint32_t repeatAt;
};
enum Phase { GREEN_13, YELLOW_13, GREEN_24, YELLOW_24 };
enum RunMode { STARTING, DAY_MODE, NIGHT_MODE };
enum MenuItem {
M_RED13, M_YELLOW13, M_GREEN13, M_RED24, M_YELLOW24, M_GREEN24,
M_CLOCK_HOUR, M_CLOCK_MINUTE, M_NIGHT_HOUR, M_NIGHT_MINUTE,
M_DAY_HOUR, M_DAY_MINUTE, M_NIGHT_ENABLED, MENU_COUNT
};
Settings settings; // Gia tri da LUU, dung cho chu ky tiep theo.
Settings running; // Ban chup thoi gian den cua chu ky dang chay.
Settings draft; // Ban nhap trong menu, chua LUU thi chua ap dung.
Settings persisted; // Ban da ghi EEPROM, tranh ghi lai khi khong doi.
bool havePersisted = false;
int8_t activeSlot = -1;
uint32_t savedSequence = 0;
Button buttons[4];
Phase phase = GREEN_13;
RunMode runMode = STARTING;
uint32_t phaseStartedAt = 0;
uint32_t modeStartedAt = 0;
uint8_t lastLightMask = 0xFF;
int lastSeg13 = -999;
int lastSeg24 = -999;
uint32_t secondsOfDay = 6UL * 3600UL;
uint32_t clockTickAt = 0;
bool clockIsSet = false;
bool menuOpen = false;
uint8_t menuIndex = M_RED13;
uint16_t draftClockMinutes = 360;
bool draftClockChanged = false;
bool menuClickPending = false;
bool clickOpenedMenu = false;
uint32_t menuClickedAt = 0;
const char *noticeText = "";
uint32_t noticeStartedAt = 0;
uint16_t noticeDuration = 0;
char lcdCache[LCD_ROWS][LCD_COLS + 1];
uint32_t lcdUpdatedAt = 0;
bool lcdDirty = true;
// Khai bao ro cac ham co kieu tu dinh nghia, tuong thich Arduino preprocessor.
bool validSettings(const Settings &s);
bool sameTrafficTimes(const Settings &a, const Settings &b);
bool readRecord(uint8_t slot, EepromRecord &record);
void initButton(Button &b, uint8_t pin, uint32_t now);
void updateButton(Button &b, uint32_t now);
bool repeatPressed(Button &b, uint32_t now);
// ============================================================================
// 03. CAI DAT MAC DINH / KIEM TRA RANG BUOC THOI GIAN
// ============================================================================
void setDefaults() {
settings.green13 = 10;
settings.yellow13 = 2;
settings.green24 = 10;
settings.yellow24 = 2;
settings.nightStart = 22 * 60;
settings.dayStart = 6 * 60;
settings.nightEnabled = 1;
}
bool validSettings(const Settings &s) {
return s.green13 >= 1 && s.yellow13 >= 1 &&
s.green24 >= 1 && s.yellow24 >= 1 &&
s.green13 <= MAX_RED_SECONDS && s.yellow13 <= MAX_RED_SECONDS &&
s.green24 <= MAX_RED_SECONDS && s.yellow24 <= MAX_RED_SECONDS &&
s.nightStart < 1440 && s.dayStart < 1440 &&
s.nightEnabled <= 1 &&
(!s.nightEnabled || s.nightStart != s.dayStart);
}
bool sameTrafficTimes(const Settings &a, const Settings &b) {
return a.green13 == b.green13 && a.yellow13 == b.yellow13 &&
a.green24 == b.green24 && a.yellow24 == b.yellow24;
}
// ============================================================================
// 04. EEPROM - 2 BAN LUU + CRC; CHI GHI KHI LUU VA CO THAY DOI
// ============================================================================
const uint16_t EEPROM_MAGIC = 0x544C;
const uint8_t EEPROM_VERSION = 3;
const uint8_t EEPROM_COMMITTED = 0xA5;
const int EEPROM_SLOT_BASE[2] = {32, 64};
static_assert(sizeof(EepromRecord) <= 32, "EEPROM slot qua nho");
uint16_t crc16(const uint8_t *bytes, size_t length) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; ++i) {
crc ^= (uint16_t)bytes[i] << 8;
for (uint8_t bit = 0; bit < 8; ++bit) {
crc = (crc & 0x8000) ? (uint16_t)((crc << 1) ^ 0x1021)
: (uint16_t)(crc << 1);
}
}
return crc;
}
bool readRecord(uint8_t slot, EepromRecord &record) {
EEPROM.get(EEPROM_SLOT_BASE[slot], record);
return record.committed == EEPROM_COMMITTED &&
record.magic == EEPROM_MAGIC && record.version == EEPROM_VERSION &&
record.crc == crc16((const uint8_t *)&record, offsetof(EepromRecord, crc)) &&
validSettings(record.data);
}
bool saveSettings() {
if (!validSettings(settings)) return false;
if (havePersisted && memcmp(&settings, &persisted, sizeof(Settings)) == 0)
return true;
const uint8_t nextSlot = activeSlot == 0 ? 1 : 0;
const int base = EEPROM_SLOT_BASE[nextSlot];
EepromRecord record = {};
record.magic = EEPROM_MAGIC;
record.version = EEPROM_VERSION;
record.sequence = savedSequence + 1;
record.data = settings;
record.crc = crc16((const uint8_t *)&record, offsetof(EepromRecord, crc));
record.committed = EEPROM_COMMITTED;
// Khong sua ban dang tot. Danh dau slot dich chua hop le truoc khi ghi.
EEPROM.update(base + offsetof(EepromRecord, committed), 0);
const uint8_t *bytes = (const uint8_t *)&record;
for (size_t i = 0; i < offsetof(EepromRecord, committed); ++i)
EEPROM.update(base + i, bytes[i]);
EEPROM.update(base + offsetof(EepromRecord, committed), EEPROM_COMMITTED);
EepromRecord check;
if (!readRecord(nextSlot, check) ||
memcmp(&record, &check, sizeof(record)) != 0) return false;
activeSlot = nextSlot;
savedSequence = record.sequence;
persisted = settings;
havePersisted = true;
return true;
}
void loadSettings() {
EepromRecord a, b;
const bool validA = readRecord(0, a);
const bool validB = readRecord(1, b);
if (validA || validB) {
const bool chooseB = validB &&
(!validA || (int32_t)(b.sequence - a.sequence) > 0);
const EepromRecord &chosen = chooseB ? b : a;
settings = chosen.data;
activeSlot = chooseB ? 1 : 0;
savedSequence = chosen.sequence;
persisted = settings;
havePersisted = true;
return;
}
setDefaults();
// Giu 4 thoi gian da luu bang code cu, neu dung dinh dang cu MAGIC=77.
if (EEPROM.read(0) == 77) {
Settings old = settings;
old.yellow13 = EEPROM.read(1);
old.green13 = EEPROM.read(2);
old.yellow24 = EEPROM.read(3);
old.green24 = EEPROM.read(4);
if (validSettings(old)) settings = old;
}
saveSettings();
}
// ============================================================================
// 05. DS1307 / KHOANG GIO NGAY DEM
// ============================================================================
const uint8_t DS1307_ADDRESS = 0x68;
uint32_t lastRtcReadAt = 0;
bool rtcPresent = false;
uint8_t bcdToDec(uint8_t value) {
return (uint8_t)((value >> 4) * 10 + (value & 0x0F));
}
uint8_t decToBcd(uint8_t value) {
return (uint8_t)(((value / 10) << 4) | (value % 10));
}
// Doc gio DS1307. Tra ve false neu I2C loi, du lieu sai, hoac bit CH=1 (RTC dang dung).
bool readDS1307Time(uint8_t &hour, uint8_t &minute, uint8_t &second) {
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write((uint8_t)0x00);
if (Wire.endTransmission() != 0) return false;
if (Wire.requestFrom(DS1307_ADDRESS, (uint8_t)3) != 3) return false;
const uint8_t secRaw = Wire.read();
const uint8_t minRaw = Wire.read();
const uint8_t hourRaw = Wire.read();
// CH=1: dao dong DS1307 dang bi dung.
if (secRaw & 0x80) return false;
second = bcdToDec(secRaw & 0x7F);
minute = bcdToDec(minRaw & 0x7F);
// Ho tro ca che do 12h va 24h, sau do quy ve 0..23.
if (hourRaw & 0x40) {
uint8_t h12 = bcdToDec(hourRaw & 0x1F);
const bool pm = hourRaw & 0x20;
if (h12 == 12) h12 = 0;
hour = (uint8_t)(h12 + (pm ? 12 : 0));
} else {
hour = bcdToDec(hourRaw & 0x3F);
}
return hour < 24 && minute < 60 && second < 60;
}
// Ghi HH:MM:00 vao DS1307 va dong thoi xoa bit CH de cho dong ho chay.
bool writeDS1307Time(uint8_t hour, uint8_t minute) {
if (hour > 23 || minute > 59) return false;
Wire.beginTransmission(DS1307_ADDRESS);
Wire.write((uint8_t)0x00);
Wire.write(decToBcd(0)); // giay = 00, CH = 0
Wire.write(decToBcd(minute));
Wire.write(decToBcd(hour)); // ep che do 24 gio
return Wire.endTransmission() == 0;
}
void updateClock(uint32_t now) {
// DS1307 chi can doc 4 lan/giay; day du de hien thi va doi che do dung moc phut.
if ((uint32_t)(now - lastRtcReadAt) < 250UL) return;
lastRtcReadAt = now;
uint8_t h, m, s;
if (readDS1307Time(h, m, s)) {
secondsOfDay = (uint32_t)h * 3600UL + (uint32_t)m * 60UL + s;
clockIsSet = true;
rtcPresent = true;
} else {
clockIsSet = false;
rtcPresent = false;
}
}
void setRtcClock(uint16_t minutes) {
const uint8_t h = (uint8_t)((minutes / 60U) % 24U);
const uint8_t m = (uint8_t)(minutes % 60U);
if (writeDS1307Time(h, m)) {
secondsOfDay = (uint32_t)h * 3600UL + (uint32_t)m * 60UL;
clockIsSet = true;
rtcPresent = true;
lastRtcReadAt = 0;
} else {
clockIsSet = false;
rtcPresent = false;
}
}
bool shouldRunNight() {
if (!clockIsSet || !settings.nightEnabled) return false;
const uint16_t current = (uint16_t)(secondsOfDay / 60UL);
const uint16_t start = settings.nightStart;
const uint16_t finish = settings.dayStart;
if (start == finish) return false;
if (start < finish) return current >= start && current < finish;
return current >= start || current < finish; // Qua nua dem, vi du 22h -> 6h.
}
// ============================================================================
// 06. NGO RA DEN - TAT TRUOC BAT SAU, KHONG CHONG MAU
// ============================================================================
// Bit: 0=Do13, 1=Vang13, 2=Xanh13, 3=Do24, 4=Vang24, 5=Xanh24.
const uint8_t R13_MASK = 1 << 0;
const uint8_t Y13_MASK = 1 << 1;
const uint8_t G13_MASK = 1 << 2;
const uint8_t R24_MASK = 1 << 3;
const uint8_t Y24_MASK = 1 << 4;
const uint8_t G24_MASK = 1 << 5;
void writeLights(uint8_t mask) {
const bool allowed = mask == 0 || mask == (R13_MASK | R24_MASK) ||
mask == (G13_MASK | R24_MASK) || mask == (Y13_MASK | R24_MASK) ||
mask == (R13_MASK | G24_MASK) || mask == (R13_MASK | Y24_MASK) ||
mask == (Y13_MASK | Y24_MASK);
if (!allowed) mask = R13_MASK | R24_MASK;
if (mask == lastLightMask) return;
// Khong ghi truc tiep tung den theo pha moi khi den cu chua tat.
for (uint8_t i = 0; i < 6; ++i) digitalWrite(LIGHT_PINS[i], LOW);
for (uint8_t i = 0; i < 6; ++i)
if (mask & (1 << i)) digitalWrite(LIGHT_PINS[i], HIGH);
lastLightMask = mask;
}
void applyDayLights() {
switch (phase) {
case GREEN_13: writeLights(G13_MASK | R24_MASK); break;
case YELLOW_13: writeLights(Y13_MASK | R24_MASK); break;
case GREEN_24: writeLights(R13_MASK | G24_MASK); break;
case YELLOW_24: writeLights(R13_MASK | Y24_MASK); break;
default: writeLights(R13_MASK | R24_MASK); break;
}
}
uint16_t phaseSeconds() {
switch (phase) {
case GREEN_13: return running.green13;
case YELLOW_13: return running.yellow13;
case GREEN_24: return running.green24;
case YELLOW_24: return running.yellow24;
}
return 1;
}
void beginDay(uint32_t now) {
runMode = STARTING;
modeStartedAt = now;
running = settings;
writeLights(R13_MASK | R24_MASK);
lcdDirty = true;
}
void updateTraffic(uint32_t now) {
if (shouldRunNight()) {
if (runMode != NIGHT_MODE) {
runMode = NIGHT_MODE;
modeStartedAt = now;
lcdDirty = true;
}
const bool on = ((uint32_t)(now - modeStartedAt) / NIGHT_BLINK_MS) % 2 == 0;
writeLights(on ? (Y13_MASK | Y24_MASK) : 0);
return;
}
if (runMode == NIGHT_MODE) beginDay(now);
if (runMode == STARTING) {
if ((uint32_t)(now - modeStartedAt) < START_RED_MS) return;
running = settings;
phase = GREEN_13;
phaseStartedAt = now;
runMode = DAY_MODE;
applyDayLights();
lcdDirty = true;
return;
}
if ((uint32_t)(now - phaseStartedAt) >= (uint32_t)phaseSeconds() * 1000UL) {
phase = (Phase)(((uint8_t)phase + 1) % 4);
// Chi doi thoi gian da luu o dau CHU KY MOI, khong cat ngang pha cu.
if (phase == GREEN_13) running = settings;
phaseStartedAt = now;
applyDayLights();
lcdDirty = true;
}
}
uint16_t phaseRemaining(uint32_t now) {
const uint32_t duration = (uint32_t)phaseSeconds() * 1000UL;
const uint32_t elapsed = (uint32_t)(now - phaseStartedAt);
if (elapsed >= duration) return 0;
return (uint16_t)((duration - elapsed + 999UL) / 1000UL);
}
void getCountdowns(uint32_t now, uint16_t &n13, uint16_t &n24,
char &color13, char &color24) {
const uint16_t left = phaseRemaining(now);
n13 = n24 = left;
color13 = color24 = 'D';
switch (phase) {
case GREEN_13: color13 = 'X'; break; // Xanh 1-3 va do 2-4 dem cung thoi gian.
case YELLOW_13: color13 = 'V'; break;
case GREEN_24: color24 = 'X'; break; // Xanh 2-4 va do 1-3 dem cung thoi gian.
case YELLOW_24: color24 = 'V'; break;
}
}
// ============================================================================
// 07. LED 7 DOAN 2 CHU SO - 7SEG-MPX2-CC, QUET MULTIPLEX
// ============================================================================
const uint8_t DIGITS[10] = {
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F
};
// Gia tri can hien thi cho moi huong. -1 = tat hoan toan.
int display13Value = -1;
int display24Value = -1;
uint8_t scanDigit = 0;
uint32_t lastSevenSegScanAt = 0;
const uint16_t SEVEN_SEG_SCAN_US = 2500; // 2.5 ms / mot buoc quet.
void disableDisplayDigits(const uint8_t digitPins[2]) {
// MPX2-CC: chan chung am LOW = bat, HIGH = tat.
digitalWrite(digitPins[0], HIGH);
digitalWrite(digitPins[1], HIGH);
}
void writeSegmentPattern(const uint8_t segPins[7], uint8_t pattern) {
for (uint8_t i = 0; i < 7; ++i) {
const bool on = (pattern & (1 << i)) != 0;
digitalWrite(segPins[i], on ? HIGH : LOW);
}
}
void scanOneDisplay(const uint8_t segPins[7], const uint8_t digitPins[2],
int value, uint8_t whichDigit) {
disableDisplayDigits(digitPins);
if (value < 0) {
writeSegmentPattern(segPins, 0);
return;
}
value = constrain(value, 0, 99);
int digitValue;
if (whichDigit == 0) {
// Chu so trai (hang chuc). Duoi 10 thi de trong cho de nhin.
if (value < 10) {
writeSegmentPattern(segPins, 0);
return;
}
digitValue = value / 10;
} else {
// Chu so phai (hang don vi).
digitValue = value % 10;
}
writeSegmentPattern(segPins, DIGITS[digitValue]);
digitalWrite(digitPins[whichDigit], LOW);
}
void refreshSevenSegmentMultiplex() {
const uint32_t us = micros();
if ((uint32_t)(us - lastSevenSegScanAt) < SEVEN_SEG_SCAN_US) return;
lastSevenSegScanAt = us;
// Tat ca chan chon truoc khi doi mau doan de tranh bong ma.
disableDisplayDigits(DIGIT13);
disableDisplayDigits(DIGIT24);
scanOneDisplay(SEG13, DIGIT13, display13Value, scanDigit);
scanOneDisplay(SEG24, DIGIT24, display24Value, scanDigit);
scanDigit ^= 1;
}
void updateSevenSegments(uint32_t now) {
int n13 = -1, n24 = -1;
if (runMode == DAY_MODE) {
uint16_t a, b;
char ca, cb;
getCountdowns(now, a, b, ca, cb);
n13 = constrain((int)a, 0, 99);
n24 = constrain((int)b, 0, 99);
}
if (n13 != lastSeg13) {
display13Value = n13;
lastSeg13 = n13;
}
if (n24 != lastSeg24) {
display24Value = n24;
lastSeg24 = n24;
}
// Quet ngay de khi vua doi gia tri man hinh khong bi tre.
refreshSevenSegmentMultiplex();
}
// ============================================================================
// 08. DOC NUT NHAN - CHONG DOI, BAT CANH NHAN, KHONG delay()
// ============================================================================
void initButton(Button &b, uint8_t pin, uint32_t now) {
b.pin = pin;
pinMode(pin, INPUT_PULLUP);
b.rawDown = b.down = digitalRead(pin) == LOW;
b.pressed = false;
b.changedAt = b.pressedAt = b.repeatAt = now;
}
void updateButton(Button &b, uint32_t now) {
b.pressed = false;
const bool raw = digitalRead(b.pin) == LOW;
if (raw != b.rawDown) { b.rawDown = raw; b.changedAt = now; }
if (raw != b.down && (uint32_t)(now - b.changedAt) >= DEBOUNCE_MS) {
b.down = raw;
if (b.down) {
b.pressed = true;
b.pressedAt = b.repeatAt = now;
}
}
}
bool repeatPressed(Button &b, uint32_t now) {
if (b.pressed) return true;
if (!b.down || (uint32_t)(now - b.pressedAt) < REPEAT_START_MS) return false;
if ((uint32_t)(now - b.repeatAt) < REPEAT_INTERVAL_MS) return false;
b.repeatAt = now;
return true;
}
// ============================================================================
// 09. MENU CAI DAT - GIU THU TU 6 DEN NHU SO DO CHAN
// ============================================================================
const char *MENU_TITLES[MENU_COUNT] = {
"DO HUONG 1-3", "VANG HUONG 1-3", "XANH HUONG 1-3",
"DO HUONG 2-4", "VANG HUONG 2-4", "XANH HUONG 2-4",
"GIO HIEN TAI", "PHUT HIEN TAI", "GIO BAT DAU DEM", "PHUT BAT DAU DEM",
"GIO BAT DAU NGAY", "PHUT BAT DAU NGAY", "TU DONG NGAY / DEM"
};
void showNotice(const char *text, uint16_t duration, uint32_t now) {
noticeText = text;
noticeDuration = duration;
noticeStartedAt = now;
lcdDirty = true;
}
bool haveNotice(uint32_t now) {
return noticeDuration && (uint32_t)(now - noticeStartedAt) < noticeDuration;
}
int menuValue() {
switch (menuIndex) {
case M_RED13: return draft.green24; // Do 1-3 = Xanh 2-4.
case M_YELLOW13: return draft.yellow13;
case M_GREEN13: return draft.green13;
case M_RED24: return draft.green13; // Do 2-4 = Xanh 1-3.
case M_YELLOW24: return draft.yellow24;
case M_GREEN24: return draft.green24;
case M_CLOCK_HOUR: return draftClockMinutes / 60;
case M_CLOCK_MINUTE: return draftClockMinutes % 60;
case M_NIGHT_HOUR: return draft.nightStart / 60;
case M_NIGHT_MINUTE: return draft.nightStart % 60;
case M_DAY_HOUR: return draft.dayStart / 60;
case M_DAY_MINUTE: return draft.dayStart % 60;
case M_NIGHT_ENABLED: return draft.nightEnabled;
}
return 0;
}
int menuMinimum() {
return menuIndex <= M_GREEN24 ? 1 : 0;
}
int menuMaximum() {
switch (menuIndex) {
case M_RED13: case M_RED24: return MAX_RED_SECONDS;
case M_YELLOW13: case M_GREEN13:
case M_YELLOW24: case M_GREEN24: return MAX_RED_SECONDS;
case M_CLOCK_HOUR: case M_NIGHT_HOUR: case M_DAY_HOUR: return 23;
case M_CLOCK_MINUTE: case M_NIGHT_MINUTE: case M_DAY_MINUTE: return 59;
case M_NIGHT_ENABLED: return 1;
}
return 0;
}
void changeMenuValue(int direction) {
int value = menuValue() + direction;
const int lo = menuMinimum(), hi = menuMaximum();
// Gio/phut quay vong; thoi gian den dung tai gioi han an toan.
if (menuIndex >= M_CLOCK_HOUR && menuIndex < M_NIGHT_ENABLED) {
if (value > hi) value = lo;
if (value < lo) value = hi;
} else {
if (value > hi) value = hi;
if (value < lo) value = lo;
}
switch (menuIndex) {
case M_RED13: draft.green24 = value; break; // Dong bo Do 1-3 voi Xanh 2-4.
case M_YELLOW13: draft.yellow13 = value; break;
case M_GREEN13: draft.green13 = value; break;
case M_RED24: draft.green13 = value; break; // Dong bo Do 2-4 voi Xanh 1-3.
case M_YELLOW24: draft.yellow24 = value; break;
case M_GREEN24: draft.green24 = value; break;
case M_CLOCK_HOUR:
draftClockMinutes = value * 60 + draftClockMinutes % 60;
draftClockChanged = true; break;
case M_CLOCK_MINUTE:
draftClockMinutes = (draftClockMinutes / 60) * 60 + value;
draftClockChanged = true; break;
case M_NIGHT_HOUR: draft.nightStart = value * 60 + draft.nightStart % 60; break;
case M_NIGHT_MINUTE: draft.nightStart = (draft.nightStart / 60) * 60 + value; break;
case M_DAY_HOUR: draft.dayStart = value * 60 + draft.dayStart % 60; break;
case M_DAY_MINUTE: draft.dayStart = (draft.dayStart / 60) * 60 + value; break;
case M_NIGHT_ENABLED: draft.nightEnabled = value; break;
}
noticeDuration = 0;
lcdDirty = true;
}
void nextMenuItem() {
menuIndex = (menuIndex + 1) % MENU_COUNT;
// Lay lai gio dang chay khi moi den muc GIO, neu chua sua ban nhap.
if (menuIndex == M_CLOCK_HOUR && !draftClockChanged)
draftClockMinutes = secondsOfDay / 60UL;
lcdDirty = true;
}
void enterMenu() {
menuOpen = true;
menuIndex = M_RED13;
draft = settings;
draftClockMinutes = secondsOfDay / 60UL;
draftClockChanged = false;
noticeDuration = 0;
lcdDirty = true;
}
void exitMenu() {
menuOpen = false;
menuClickPending = false;
draftClockChanged = false;
noticeDuration = 0;
lcdDirty = true; // Khong sua moc thoi gian den, khong tam dung den.
}
void saveMenu(uint32_t now) {
if (!validSettings(draft)) {
showNotice("GIO NGAY = GIO DEM!", 2500, now);
return;
}
const Settings previous = settings;
settings = draft;
if (!saveSettings()) {
settings = previous;
showNotice("LOI LUU EEPROM!", 2500, now);
return;
}
if (draftClockChanged || menuIndex == M_CLOCK_HOUR || menuIndex == M_CLOCK_MINUTE) {
setRtcClock(draftClockMinutes);
draftClockChanged = false;
}
draft = settings;
nextMenuItem();
showNotice(sameTrafficTimes(settings, running) ? "DA LUU. MUC TIEP THEO"
: "DA LUU: CHO CHU KY", 1500, millis());
}
void handleButtons(uint32_t now) {
for (uint8_t i = 0; i < 4; ++i) updateButton(buttons[i], now);
if (buttons[0].pressed) {
if (!menuOpen) {
enterMenu();
menuClickPending = true;
clickOpenedMenu = true;
menuClickedAt = now;
} else if (menuClickPending && (uint32_t)(now - menuClickedAt) <= DOUBLE_CLICK_MS) {
exitMenu();
} else {
menuClickPending = true;
clickOpenedMenu = false;
menuClickedAt = now;
}
}
if (!menuOpen) return;
// TANG/GIAM cung luc: khong thay doi. MENU/LUU khong lap khi giu nut.
const bool up = repeatPressed(buttons[1], now);
const bool down = repeatPressed(buttons[2], now);
if ((up && !buttons[2].down) || (down && !buttons[1].down)) {
menuClickPending = false;
changeMenuValue(up ? 1 : -1);
}
if (buttons[3].pressed) {
menuClickPending = false;
saveMenu(now);
}
if (menuClickPending && (uint32_t)(now - menuClickedAt) > DOUBLE_CLICK_MS) {
if (!clickOpenedMenu) nextMenuItem();
menuClickPending = false;
}
}
// ============================================================================
// 10. LCD 20x4 - KHONG clear() LIEN TUC; CHI GHI DONG THAY DOI
// ============================================================================
void printLine(uint8_t row, const char *text) {
if (row >= LCD_ROWS) return;
char padded[LCD_COLS + 1];
memset(padded, ' ', LCD_COLS);
padded[LCD_COLS] = '\0';
size_t length = strlen(text);
if (length > LCD_COLS) length = LCD_COLS;
memcpy(padded, text, length);
if (memcmp(lcdCache[row], padded, LCD_COLS + 1) == 0) return;
lcd.setCursor(0, row);
lcd.print(padded);
memcpy(lcdCache[row], padded, LCD_COLS + 1);
}
void showMainLCD(uint32_t now) {
char line[40];
printLine(0, " DEN GIAO THONG");
snprintf(line, sizeof(line), "%02u:%02u:%02u %s",
(unsigned)(secondsOfDay / 3600UL), (unsigned)((secondsOfDay / 60UL) % 60),
(unsigned)(secondsOfDay % 60), runMode == NIGHT_MODE ? "DEM" : "NGAY");
printLine(1, line);
if (runMode == NIGHT_MODE) printLine(2, "2 HUONG NHAY VANG");
else if (runMode == STARTING) printLine(2, "KHOI DONG: CUNG DO");
else {
uint16_t a, b;
char ca, cb;
getCountdowns(now, a, b, ca, cb);
snprintf(line, sizeof(line), "13:%c %02us 24:%c %02us", ca, (unsigned)a, cb, (unsigned)b);
printLine(2, line);
}
if (!clockIsSet) printLine(3, "RTC LOI / DAT GIO MENU");
else if (!sameTrafficTimes(settings, running) && runMode == DAY_MODE)
printLine(3, "DA LUU: CHO CHU KY");
else if (!settings.nightEnabled) printLine(3, "TU DONG DEM: TAT");
else {
snprintf(line, sizeof(line), "DEM %02u:%02u NGAY %02u:%02u",
(unsigned)(settings.nightStart / 60), (unsigned)(settings.nightStart % 60),
(unsigned)(settings.dayStart / 60), (unsigned)(settings.dayStart % 60));
printLine(3, line);
}
}
void showMenuLCD(uint32_t now) {
char line[40];
const bool changed = draftClockChanged || memcmp(&draft, &settings, sizeof(Settings)) != 0;
snprintf(line, sizeof(line), "CAI DAT %02u/%02u %c", (unsigned)(menuIndex + 1),
(unsigned)MENU_COUNT, changed ? '*' : ' ');
printLine(0, line);
printLine(1, MENU_TITLES[menuIndex]);
if (menuIndex <= M_GREEN24) {
snprintf(line, sizeof(line), "Gia tri: %02u giay", (unsigned)menuValue());
} else if (menuIndex <= M_CLOCK_MINUTE) {
snprintf(line, sizeof(line), "Gio: %02u:%02u (%s)", (unsigned)(draftClockMinutes / 60),
(unsigned)(draftClockMinutes % 60), menuIndex == M_CLOCK_HOUR ? "GIO" : "PHUT");
} else if (menuIndex <= M_NIGHT_MINUTE) {
snprintf(line, sizeof(line), "Bat dau dem: %02u:%02u", (unsigned)(draft.nightStart / 60),
(unsigned)(draft.nightStart % 60));
} else if (menuIndex <= M_DAY_MINUTE) {
snprintf(line, sizeof(line), "Bat dau ngay: %02u:%02u", (unsigned)(draft.dayStart / 60),
(unsigned)(draft.dayStart % 60));
} else {
snprintf(line, sizeof(line), "Trang thai: %s", draft.nightEnabled ? "BAT" : "TAT");
}
printLine(2, line);
if (haveNotice(now)) printLine(3, noticeText);
else if (menuIndex == M_RED13) printLine(3, "D13 = X24");
else if (menuIndex == M_RED24) printLine(3, "D24 = X13");
else printLine(3, "+/-:SUA LUU:Luu het");
}
void updateLCD(uint32_t now) {
if (!lcdDirty && (uint32_t)(now - lcdUpdatedAt) < LCD_REFRESH_MS) return;
lcdUpdatedAt = now;
lcdDirty = false;
if (menuOpen) showMenuLCD(now);
else showMainLCD(now);
}
// ============================================================================
// 11. SETUP / LOOP
// ============================================================================
void setup() {
for (uint8_t i = 0; i < 6; ++i) {
digitalWrite(LIGHT_PINS[i], LOW);
pinMode(LIGHT_PINS[i], OUTPUT);
}
writeLights(R13_MASK | R24_MASK);
for (uint8_t i = 0; i < 7; ++i) {
digitalWrite(SEG13[i], LOW);
digitalWrite(SEG24[i], LOW);
pinMode(SEG13[i], OUTPUT);
pinMode(SEG24[i], OUTPUT);
}
for (uint8_t i = 0; i < 2; ++i) {
// MPX2-CC: HIGH = tat chu so, LOW = bat chu so.
digitalWrite(DIGIT13[i], HIGH);
digitalWrite(DIGIT24[i], HIGH);
pinMode(DIGIT13[i], OUTPUT);
pinMode(DIGIT24[i], OUTPUT);
}
loadSettings();
Wire.begin();
Wire.setClock(100000UL); // DS1307 dung I2C Standard-mode 100 kHz.
// Timeout la bao ve bus I2C neu core AVR dang dung co ho tro.
#if defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(25000UL, true);
#endif
lcd.init();
lcd.backlight();
lcd.clear();
memset(lcdCache, 0, sizeof(lcdCache));
const uint32_t now = millis();
initButton(buttons[0], BTN_MENU, now);
initButton(buttons[1], BTN_UP, now);
initButton(buttons[2], BTN_DOWN, now);
initButton(buttons[3], BTN_SAVE, now);
secondsOfDay = 6UL * 3600UL;
clockTickAt = now; // Giu bien de tuong thich cau truc code cu, RTC khong dung bien nay.
clockIsSet = false;
lastRtcReadAt = now - 1000UL;
updateClock(now);
beginDay(now);
updateSevenSegments(now);
updateLCD(now);
}
void loop() {
updateClock(millis());
updateTraffic(millis());
handleButtons(millis());
// Doc lai millis sau thao tac EEPROM, tranh su dung moc cu de tinh gio.
updateClock(millis());
updateTraffic(millis());
updateSevenSegments(millis());
// Quet multiplex phai chay lien tuc de 2 LED 2 chu so khong bi nhap nhay.
refreshSevenSegmentMultiplex();
updateLCD(millis());
}