commit d040d6a886d51dc768134d8d5c55368c151a01c7 Author: Niklas Brunke Date: Thu Jan 22 10:26:50 2026 +0100 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/borrow-system-locker-esp32.ino b/borrow-system-locker-esp32.ino new file mode 100644 index 0000000..c5b746b --- /dev/null +++ b/borrow-system-locker-esp32.ino @@ -0,0 +1,416 @@ +#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 = 5000; // 5 seconds in milliseconds + +// 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 + +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(); + } + } + + // Check if keypad button was pressed + keypad.updateFIFO(); + char button = keypad.getButton(); + + if (button != 0 && !processingLoan) { + 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 { + showError("Code muss 6-stellig sein!"); + 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() < 6) { + currentCode += button; + updateCodeDisplay(); + } + } +} + +void updateCodeDisplay() { + lcd.setCursor(0, 1); + // Display asterisks for entered digits + String display = ""; + for (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("Ungültiger 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("Rückgabe"); + } else { + lcd.print("Ausleihe"); + } + + // Open doors one by one + JsonArray lockerArray = doc.as(); + int doorCount = 0; + + for (JsonVariant locker : lockerArray) { + int doorNumber = locker.as(); + if (doorNumber >= 1 && doorNumber <= 6) { + doorCount++; + openDoor(doorNumber); + } + } + + if (doorCount == 0) { + showError("Keine Fächer!"); + resetState(); + return; + } + + // Update loan status in backend + if (isReturn) { + setReturnDate(code); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Rückgabe OK!"); + } else { + setTakeDate(code); + lcd.clear(); + lcd.setCursor(0, 0); + lcd.print("Ausleihe 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); + 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; +} + +void openDoor(int doorNumber) { + if (doorNumber < 1 || doorNumber > 6) { + return; + } + + int pinIndex = doorNumber - 1; + + lcd.setCursor(0, 1); + lcd.print("Öffne Fach "); + lcd.print(doorNumber); + lcd.print(" "); + + Serial.println("Opening door " + String(doorNumber)); + + // Activate relay (LOW = on for most relay modules) + digitalWrite(RELAY_PINS[pinIndex], LOW); + + // Keep door open for specified time + delay(DOOR_OPEN_TIME); + + // Deactivate relay (close door) + digitalWrite(RELAY_PINS[pinIndex], HIGH); + + Serial.println("Door " + String(doorNumber) + " closed"); +} + +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); + 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); + 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; + 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); + } +} \ No newline at end of file diff --git a/config.h.example b/config.h.example new file mode 100644 index 0000000..753c5b1 --- /dev/null +++ b/config.h.example @@ -0,0 +1,15 @@ +// config.h.example - Template configuration file +// Copy this file to config.h and fill in your actual values + +#ifndef CONFIG_H +#define CONFIG_H + +// WiFi credentials +const char* WIFI_SSID = "YOUR_WIFI_SSID"; +const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; + +// Backend API configuration +const char* BACKEND_URL = "https://insta.the1s.de/backend/api"; +const char* API_KEY = "12345678"; // Replace with your 8-digit API key + +#endif // CONFIG_H