#include <Keypad.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <FS.h>
#include <LittleFS.h>
// --- Configurable Constants ---
#define PASSCODE_FILE "/pass.txt"
#define DEFAULT_CODE "#D36"
#define HOLD_TIME_MS 2500
#define RELAY_PIN 5 // Relay successfully moved to safe GPIO 5
#define RELAY_ON_TIME_MS 3000 // Relay trigger duration (3 seconds)
// --- Keypad Setup ---
const byte ROWS = 4;
const byte COLS = 4;
char keys[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
byte rowPins[ROWS] = {13, 12, 14, 27};
byte colPins[COLS] = {26, 25, 33, 32};
Keypad customKeypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS);
// --- TFT Setup ---
TFT_eSPI tft = TFT_eSPI();
// --- State Machine ---
enum SystemState {
STATE_IDLE,
STATE_ENTER_PASSCODE,
STATE_AUTH_FOR_CHANGE,
STATE_CHANGE_PASSCODE,
STATE_VERIFY_PASSCODE
};
SystemState currentState = STATE_IDLE;
String currentPasscode = DEFAULT_CODE;
String enteredInput = "";
String newPasscodeAttempt = "";
// Timing variables for holding '#'
unsigned long hashPressStartTime = 0;
bool isHashBeingHeld = false;
void setup() {
Serial.begin(115200);
// Set up the hardware relay pin control
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, LOW); // Start with relay deactivated (Locked)
tft.init();
tft.setRotation(0);
if (!LittleFS.begin(true)) {
Serial.println("LittleFS Mount Failed!");
} else {
loadPasscode();
}
showIdleScreen();
}
void loop() {
char key = customKeypad.getKey();
KeyState kstate = customKeypad.getState();
// --- 1. SPECIAL '#' HANDLING IN IDLE STATE ---
if (currentState == STATE_IDLE) {
if (key == '#' && kstate == PRESSED) {
isHashBeingHeld = true;
hashPressStartTime = millis();
}
if (isHashBeingHeld && (kstate == HOLD || kstate == PRESSED)) {
if (millis() - hashPressStartTime >= HOLD_TIME_MS) {
isHashBeingHeld = false;
enteredInput = "";
currentState = STATE_AUTH_FOR_CHANGE;
updateDisplay();
return;
}
}
if (kstate == RELEASED && isHashBeingHeld) {
isHashBeingHeld = false;
currentState = STATE_ENTER_PASSCODE;
enteredInput = "#";
updateDisplay();
return;
}
}
// --- 2. PROCESS NORMAL KEYPRESSES ---
if (key != NO_KEY) {
if (currentState == STATE_IDLE && key != '#') {
currentState = STATE_ENTER_PASSCODE;
enteredInput = String(key);
updateDisplay();
return;
}
if (currentState == STATE_ENTER_PASSCODE || currentState == STATE_AUTH_FOR_CHANGE ||
currentState == STATE_CHANGE_PASSCODE || currentState == STATE_VERIFY_PASSCODE) {
if (enteredInput.length() < 4) {
enteredInput += key;
updateDisplay();
}
if (enteredInput.length() == 4) {
delay(400);
if (currentState == STATE_ENTER_PASSCODE) {
if (enteredInput == currentPasscode) {
triggerRelay();
} else {
flashMessage("WRONG PASSCODE", TFT_RED);
}
resetToIdle();
}
else if (currentState == STATE_AUTH_FOR_CHANGE) {
if (enteredInput == currentPasscode) {
enteredInput = "";
currentState = STATE_CHANGE_PASSCODE;
updateDisplay();
} else {
flashMessage("WRONG PASSCODE", TFT_RED);
resetToIdle();
}
}
else if (currentState == STATE_CHANGE_PASSCODE) { // Safe local matching string
newPasscodeAttempt = enteredInput;
enteredInput = "";
currentState = STATE_VERIFY_PASSCODE;
updateDisplay();
}
else if (currentState == STATE_VERIFY_PASSCODE) {
if (enteredInput == newPasscodeAttempt) {
currentPasscode = newPasscodeAttempt;
savePasscode(currentPasscode);
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.drawString("Passcode Changed", 120, 100, 4);
tft.drawString(currentPasscode, 120, 140, 4);
delay(2500);
resetToIdle();
} else {
flashMessage("MISMATCH! TRY AGAIN", TFT_RED);
enteredInput = "";
currentState = STATE_CHANGE_PASSCODE;
updateDisplay();
}
}
}
}
}
}
// --- 3. Hardware Interfacing Actions ---
void triggerRelay() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_GREEN, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.drawString("ACCESS GRANTED", 120, 120, 4);
digitalWrite(RELAY_PIN, HIGH); // Turn relay ON (Unlock fence)
delay(RELAY_ON_TIME_MS); // Hold for 3 seconds
digitalWrite(RELAY_PIN, LOW); // Turn relay OFF (Lock fence)
}
// --- 4. Screen Drawing ---
void showIdleScreen() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.drawString("10:08", 120, 120, 7);
}
void updateDisplay() {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_WHITE, TFT_BLACK);
tft.setTextDatum(TC_DATUM);
if (currentState == STATE_ENTER_PASSCODE) {
tft.drawString("Enter Passcode", 120, 50, 4);
} else if (currentState == STATE_AUTH_FOR_CHANGE) {
tft.drawString("Enter Passcode", 120, 35, 4);
tft.drawString("To Change Passcode", 120, 65, 2);
} else if (currentState == STATE_CHANGE_PASSCODE) {
tft.drawString("Change Passcode", 120, 50, 4);
} else if (currentState == STATE_VERIFY_PASSCODE) {
tft.drawString("Re-enter Passcode", 120, 50, 4);
}
int startX = 60;
int spacing = 40;
tft.setTextDatum(MC_DATUM);
for (int i = 0; i < 4; i++) {
int currentX = startX + (i * spacing);
if (i < enteredInput.length()) {
tft.drawString("*", currentX, 130, 4);
} else {
tft.drawString("_", currentX, 130, 4);
}
}
}
void flashMessage(String msg, uint16_t color) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(color, TFT_BLACK);
tft.setTextDatum(MC_DATUM);
tft.drawString(msg, 120, 120, 4);
delay(2000);
}
void resetToIdle() {
enteredInput = "";
newPasscodeAttempt = "";
isHashBeingHeld = false;
currentState = STATE_IDLE;
showIdleScreen();
}
// --- 5. Storage Persistence Layer ---
void loadPasscode() {
if (LittleFS.exists(PASSCODE_FILE)) {
File file = LittleFS.open(PASSCODE_FILE, "r");
if (file) {
String saved = file.readStringUntil('\n');
saved.trim();
if (saved.length() == 4) {
currentPasscode = saved;
Serial.println("Loaded custom passcode: " + currentPasscode);
}
file.close();
}
}
}
void savePasscode(String newCode) {
File file = LittleFS.open(PASSCODE_FILE, "w");
if (file) {
file.println(newCode);
file.close();
Serial.println("Saved custom passcode to LittleFS: " + newCode);
}
}