#include #include #include #include #include #include #include "config.h" // Include private configuration // Hardware configuration const int RELAY_PINS[6] = {13, 12, 14, 27, 26, 25}; // Relay pins for doors 1-6 const int DOOR_OPEN_TIME = 10000; // I2C devices LiquidCrystal_I2C lcd(0x27, 16, 2); // LCD with I2C address 0x27 KEYPAD keypad; // Qwiic Keypad // State variables String currentCode = ""; bool processingLoan = false; int failedAttempts = 0; unsigned long lockoutEndTime = 0; const int MAX_FAILED_ATTEMPTS = 3; const unsigned long LOCKOUT_DURATION = 30000; // 30 seconds in milliseconds // Door control state struct DoorState { bool isOpen; unsigned long openedAt; int doorNumber; }; DoorState doorStates[6] = {{false, 0, 0}}; bool doorsActive = false; void setup() { Serial.begin(115200); // Initialize I2C Wire.begin(); // Initialize LCD lcd.init(); lcd.backlight(); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Starte..."); // Initialize keypad if (!keypad.begin()) { Serial.println("Keypad not connected!"); lcd.clear(); lcd.print("Keypad Fehler!"); while (1); } // Initialize relay pins (active LOW for most relay modules) for (int i = 0; i < 6; i++) { pinMode(RELAY_PINS[i], OUTPUT); digitalWrite(RELAY_PINS[i], HIGH); // Keep doors locked initially } // Connect to WiFi connectToWiFi(); // Ready message lcd.clear(); lcd.setCursor(0, 0); lcd.print("Code eingeben:"); lcd.setCursor(0, 1); lcd.print(""); // Code will appear here Serial.println("System ready!"); } void loop() { // Check for lockout timeout if (lockoutEndTime > 0) { if (millis() < lockoutEndTime) { // Still in lockout period unsigned long remainingSeconds = (lockoutEndTime - millis()) / 1000; lcd.setCursor(0, 1); lcd.print("Warte "); lcd.print(remainingSeconds); lcd.print("s "); delay(1000); return; } else { // Lockout period ended lockoutEndTime = 0; failedAttempts = 0; resetState(); } } // Handle door timers (non-blocking) handleDoorTimers(); // Check if keypad button was pressed keypad.updateFIFO(); char button = keypad.getButton(); if (button != 0 && !processingLoan && !doorsActive) { handleKeypadInput(button); } // Check WiFi connection if (WiFi.status() != WL_CONNECTED) { connectToWiFi(); } } void handleKeypadInput(char button) { if (button == '#') { // Submit code if (currentCode.length() == 6) { processLoanCode(currentCode); } else if (currentCode.length() == 8) { // Check for hardcode checkHardcode(currentCode); } else { showError("Code: 6 oder 8 Ziffern!"); currentCode = ""; updateCodeDisplay(); } } else if (button == '*') { // Clear current input currentCode = ""; updateCodeDisplay(); lcd.setCursor(0, 0); lcd.print("Code eingeben: "); } else { // Add digit to code if (currentCode.length() < 8) { currentCode += button; updateCodeDisplay(); } } } void updateCodeDisplay() { lcd.setCursor(0, 1); // Display asterisks for entered digits String display = ""; for (unsigned int i = 0; i < currentCode.length(); i++) { display += "*"; } // Pad with spaces while (display.length() < 16) { display += " "; } lcd.print(display); } void processLoanCode(String code) { processingLoan = true; lcd.clear(); lcd.setCursor(0, 0); lcd.print("Verarbeite..."); Serial.println("Processing loan code: " + code); // Get loan information from backend String lockers = ""; bool isReturn = false; if (!getLoanInfo(code, lockers, isReturn)) { failedAttempts++; if (failedAttempts >= MAX_FAILED_ATTEMPTS) { // Start lockout period lockoutEndTime = millis() + LOCKOUT_DURATION; lcd.clear(); lcd.setCursor(0, 0); lcd.print("Zu viele Fehler!"); lcd.setCursor(0, 1); lcd.print("Gesperrt 30s"); delay(2000); return; } else { showError("Ungueltiger Code!"); lcd.setCursor(0, 1); lcd.print("Versuch "); lcd.print(failedAttempts); lcd.print("/"); lcd.print(MAX_FAILED_ATTEMPTS); delay(2000); } resetState(); return; } // Code was correct - reset failed attempts failedAttempts = 0; // Parse locker numbers DynamicJsonDocument doc(512); DeserializationError error = deserializeJson(doc, lockers); if (error) { Serial.println("JSON parsing failed!"); showError("System Fehler!"); resetState(); return; } // Display action type lcd.clear(); lcd.setCursor(0, 0); if (isReturn) { lcd.print("Rueckgabe"); } else { lcd.print("Ausleihe"); } // Collect all door numbers to open JsonArray lockerArray = doc.as(); int doorNumbers[6]; int doorCount = 0; for (JsonVariant locker : lockerArray) { int doorNumber = locker.as(); if (doorNumber >= 1 && doorNumber <= 6 && doorCount < 6) { doorNumbers[doorCount] = doorNumber; doorCount++; } } if (doorCount == 0) { showError("Keine Faecher!"); resetState(); return; } // Open all doors simultaneously (non-blocking) openDoorsSimultaneous(doorNumbers, doorCount); // Update loan status in backend if (isReturn) { setReturnDate(code); } else { setTakeDate(code); } // Wait for all doors to close, then show completion message // This will be handled by handleDoorTimers() and finishLoanProcess() processingLoan = false; } void openDoorsSimultaneous(int doorNumbers[], int count) { doorsActive = true; unsigned long now = millis(); // Display which doors are opening lcd.setCursor(0, 1); lcd.print("Fach: "); // Show door numbers for (int i = 0; i < count && i < 4; i++) { // Max 4 doors shown due to LCD width lcd.print(doorNumbers[i]); if (i < count - 1) lcd.print(","); } if (count > 4) { lcd.print("..."); } // Pad with spaces String padding = ""; for (int i = 0; i < (10 - count * 2); i++) { padding += " "; } lcd.print(padding); // Open all doors at the same time for (int i = 0; i < count; i++) { int doorNumber = doorNumbers[i]; if (doorNumber >= 1 && doorNumber <= 6) { int pinIndex = doorNumber - 1; // Activate relay (LOW = on for most relay modules) digitalWrite(RELAY_PINS[pinIndex], LOW); // Track door state doorStates[pinIndex].isOpen = true; doorStates[pinIndex].openedAt = now; doorStates[pinIndex].doorNumber = doorNumber; Serial.println("Opened door " + String(doorNumber)); } } } void handleDoorTimers() { if (!doorsActive) return; unsigned long now = millis(); bool anyDoorOpen = false; // Check each door for (int i = 0; i < 6; i++) { if (doorStates[i].isOpen) { // Check if door should be closed if (now - doorStates[i].openedAt >= DOOR_OPEN_TIME) { // Close door digitalWrite(RELAY_PINS[i], HIGH); doorStates[i].isOpen = false; Serial.println("Closed door " + String(doorStates[i].doorNumber)); } else { anyDoorOpen = true; } } } // If no doors are open anymore, finish the process if (!anyDoorOpen) { doorsActive = false; finishLoanProcess(); } } void finishLoanProcess() { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Vorgang OK!"); lcd.setCursor(0, 1); lcd.print("Vielen Dank!"); delay(3000); resetState(); } bool getLoanInfo(String loanCode, String &lockers, bool &isReturn) { if (WiFi.status() != WL_CONNECTED) { Serial.println("WiFi not connected!"); return false; } HTTPClient http; String url = String(BACKEND_URL) + "/get-loan-by-code/" + API_KEY + "/" + loanCode; http.begin(url); http.setTimeout(10000); // 10 second timeout int httpCode = http.GET(); if (httpCode != 200) { Serial.println("HTTP error: " + String(httpCode)); http.end(); return false; } String payload = http.getString(); http.end(); // Parse response DynamicJsonDocument doc(1024); DeserializationError error = deserializeJson(doc, payload); if (error) { Serial.println("JSON parsing failed!"); return false; } // Check if loan was found if (!doc.containsKey("data")) { Serial.println("No data in response"); return false; } JsonObject data = doc["data"]; // Get lockers array as string lockers = data["lockers"].as(); // Check if this is a return (returned_date is null) or pickup (returned_date is set) // If returned_date is null, this is a return action // If take_date is null, this is a pickup action isReturn = !data["returned_date"].isNull(); // Actually, based on API: if returned_date is null, user is returning items // If take_date is null, user is taking items if (data["take_date"].isNull()) { isReturn = false; // User is taking items } else if (data["returned_date"].isNull()) { isReturn = true; // User is returning items } else { // Both dates set - loan already completed Serial.println("Loan already completed"); return false; } Serial.println("Loan info retrieved. Lockers: " + lockers + ", isReturn: " + String(isReturn)); return true; } bool setReturnDate(String loanCode) { if (WiFi.status() != WL_CONNECTED) { return false; } HTTPClient http; String url = String(BACKEND_URL) + "/set-return-date/" + API_KEY + "/" + loanCode; http.begin(url); http.setTimeout(10000); // 10 second timeout int httpCode = http.POST(""); bool success = (httpCode == 200); http.end(); Serial.println("Set return date: " + String(success ? "success" : "failed")); return success; } bool setTakeDate(String loanCode) { if (WiFi.status() != WL_CONNECTED) { return false; } HTTPClient http; String url = String(BACKEND_URL) + "/set-take-date/" + API_KEY + "/" + loanCode; http.begin(url); http.setTimeout(10000); // 10 second timeout int httpCode = http.POST(""); bool success = (httpCode == 200); http.end(); Serial.println("Set take date: " + String(success ? "success" : "failed")); return success; } void showError(String message) { lcd.clear(); lcd.setCursor(0, 0); lcd.print("FEHLER:"); lcd.setCursor(0, 1); lcd.print(message); Serial.println("Error: " + message); delay(2000); } void resetState() { currentCode = ""; processingLoan = false; doorsActive = false; lcd.clear(); lcd.setCursor(0, 0); lcd.print("Code eingeben:"); lcd.setCursor(0, 1); lcd.print(""); } void connectToWiFi() { lcd.clear(); lcd.setCursor(0, 0); lcd.print("Verbinde WiFi..."); Serial.print("Connecting to WiFi"); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); Serial.print("."); lcd.setCursor(0, 1); for (int i = 0; i < (attempts % 16); i++) { lcd.print("."); } attempts++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("\nConnected to WiFi!"); Serial.println("IP: " + WiFi.localIP().toString()); lcd.clear(); lcd.setCursor(0, 0); lcd.print("WiFi verbunden!"); delay(1500); } else { Serial.println("\nFailed to connect to WiFi!"); lcd.clear(); lcd.setCursor(0, 0); lcd.print("WiFi Fehler!"); delay(2000); } } void checkHardcode(String code) { Serial.println("Checking hardcode: " + code); // Check each door's hardcode for (int i = 0; i < 6; i++) { if (code == String(DOOR_HARDCODES[i])) { // Hardcode matched for door i+1 Serial.println("Hardcode match for door " + String(i + 1)); // Reset failed attempts on successful hardcode failedAttempts = 0; lcd.clear(); lcd.setCursor(0, 0); lcd.print("Notcode erkannt"); lcd.setCursor(0, 1); lcd.print("Fach: "); lcd.print(i + 1); delay(1000); // Open only this door int doorNumber = i + 1; openDoorsSimultaneous(&doorNumber, 1); // Don't update backend (emergency access) processingLoan = false; return; } } // No hardcode matched failedAttempts++; if (failedAttempts >= MAX_FAILED_ATTEMPTS) { lockoutEndTime = millis() + LOCKOUT_DURATION; lcd.clear(); lcd.setCursor(0, 0); lcd.print("Zu viele Fehler!"); lcd.setCursor(0, 1); lcd.print("Gesperrt 30s"); delay(2000); } else { showError("Ungueltiger Code!"); lcd.setCursor(0, 1); lcd.print("Versuch "); lcd.print(failedAttempts); lcd.print("/"); lcd.print(MAX_FAILED_ATTEMPTS); delay(2000); } resetState(); }