#include <Arduino.h>
/*
* ╔══════════════════════════════════════════════════════════════╗
* ║ BIBLIOTHÈQUE INTELLIGENTE CONNECTÉE — v3.0 ║
* ║ ESP32 + FirebaseClient by mobizt (v2.2.9) ║
* ║ Auth : LegacyToken (Database Secret) ║
* ║ Simulation : Wokwi (diagram.json v1) ║
* ╚══════════════════════════════════════════════════════════════╝
*
* PIN MAP (from diagram.json)
* ─────────────────────────────────────────────────────────────
* RFID RC522 SDA=5 SCK=18 MOSI=23 MISO=19 RST=2
* LCD 1602 I2C SDA=21 SCL=22 (I2C addr 0x27)
* Servo GPIO 17
* Buzzer GPIO 16
* PIR slot 1 GPIO 4
* PIR slot 2 GPIO 13
* RGB LED R=25 G=26 B=27 (cathode commune)
* BTN EMPRUNT GPIO 33 (btn1 vert, INPUT_PULLUP)
* BTN RESET GPIO 32 (btn2 bleu, INPUT_PULLUP)
* BTN RETOUR GPIO 14 (btn3 rouge, INPUT_PULLUP)
*
* LIBRARIES REQUIRED (libraries.txt / Wokwi Library Manager)
* ─────────────────────────────────────────────────────────────
* FirebaseClient (mobizt) >= 2.2.9
* MFRC522 (miguelbalboa)
* LiquidCrystal I2C
* ESP32Servo
*
* FIREBASE SETUP
* ─────────────────────────────────────────────────────────────
* Firebase Console → Project Settings → Service Accounts
* → Database secrets → copy secret below
* No Authentication service required for LegacyToken!
* ═══════════════════════════════════════════════════════════════
*/
// ── MUST be defined BEFORE #include <FirebaseClient.h> ─────────
#define ENABLE_DATABASE
#define ENABLE_LEGACY_TOKEN
// ── Includes ───────────────────────────────────────────────────
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <FirebaseClient.h> // mobizt v2.2.9
#include <SPI.h>
#include <MFRC522.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include <ESP32Servo.h>
#include <time.h>
// ══════════════════════════════════════════════════════════════
// USER CONFIG — fill these in before running
// ══════════════════════════════════════════════════════════════
#define WIFI_SSID "Wokwi-GUEST"
#define WIFI_PASSWORD ""
// Firebase Console → Project Settings → Service Accounts → Database secrets
#define DATABASE_SECRET "sP85y8To801ZnDDEoTYHGptNpBBCim5fiU4iXHXl"
#define FIREBASE_DB_URL "https://smart-library-tse-default-rtdb.europe-west1.firebasedatabase.app"
// ── NTP (Algeria = UTC+1, no DST) ──────────────────────────────
#define NTP_TZ "CET-1"
#define NTP_SERVER "pool.ntp.org"
// ══════════════════════════════════════════════════════════════
// PIN DEFINITIONS
// ══════════════════════════════════════════════════════════════
#define PIN_RFID_SDA 5
#define PIN_RFID_SCK 18
#define PIN_RFID_MOSI 23
#define PIN_RFID_MISO 19
#define PIN_RFID_RST 2
#define PIN_LCD_SDA 21
#define PIN_LCD_SCL 22
#define PIN_SERVO 17
#define PIN_BUZZER 16
#define PIN_PIR1 4 // étagère slot 1
#define PIN_PIR2 13 // étagère slot 2
#define PIN_RGB_R 25
#define PIN_RGB_G 26
#define PIN_RGB_B 27
#define PIN_BTN_EMPRUNT 33 // btn1 vert
#define PIN_BTN_RESET 32 // btn2 bleu
#define PIN_BTN_RETOUR 14 // btn3 rouge
// ══════════════════════════════════════════════════════════════
// TIMEOUTS & SERVO ANGLES
// ══════════════════════════════════════════════════════════════
#define TIMEOUT_MENU 30000UL
#define TIMEOUT_SCAN_LIVRE 15000UL
#define TIMEOUT_DEPOT_LIVRE 20000UL
#define COUNTDOWN_FERMETURE 5000UL
#define SERVO_CLOSED 0
#define SERVO_OPEN 90
// ══════════════════════════════════════════════════════════════
// DONNÉES MEMBRES & LIVRES
// ══════════════════════════════════════════════════════════════
struct Member { const char* uid; const char* name; };
struct Book { const char* uid; const char* title; uint8_t slot; };
const Member MEMBERS[] = {
{ "01:02:03:04", "Ahmed" },
{ "55:66:77:88", "Sara" },
{ "DE:AD:BE:EF", "Youcef" },
{ "CA:FE:BA:BE", "Lina" }
};
const uint8_t MEMBER_COUNT = sizeof(MEMBERS) / sizeof(MEMBERS[0]);
const Book BOOKS[] = {
{ "11:22:33:44", "Algo & DS", 1 },
{ "C0:FF:EE:99", "Reseaux", 2 },
};
const uint8_t BOOK_COUNT = sizeof(BOOKS) / sizeof(BOOKS[0]);
// ══════════════════════════════════════════════════════════════
// FIREBASE OBJECTS — v2.x LegacyToken API
// ══════════════════════════════════════════════════════════════
// Forward-declare async callback
void asyncCB(AsyncResult& aResult);
// LegacyToken auth — uses Database Secret, no identitytoolkit needed
LegacyToken legacy_token(DATABASE_SECRET);
FirebaseApp fbApp;
// Network / SSL
WiFiClientSecure ssl_client;
// v2.x: alias required by the library
using AsyncClient = AsyncClientClass;
AsyncClient aClient(ssl_client);
// Realtime Database service object
RealtimeDatabase Database;
bool fbReady = false;
// ══════════════════════════════════════════════════════════════
// HARDWARE OBJECTS
// ══════════════════════════════════════════════════════════════
MFRC522 rfid(PIN_RFID_SDA, PIN_RFID_RST);
LiquidCrystal_I2C lcd(0x27, 16, 2);
Servo doorServo;
// ══════════════════════════════════════════════════════════════
// STATE MACHINE
// ══════════════════════════════════════════════════════════════
enum State {
STATE_IDLE,
STATE_MENU,
STATE_EMPRUNT_PORTE,
STATE_EMPRUNT_RETIRE,
STATE_EMPRUNT_ALERTE,
STATE_RETOUR_SCAN,
STATE_RETOUR_PORTE,
STATE_RETOUR_ALERTE,
STATE_RETOUR_COUNTDOWN
};
State currentState = STATE_IDLE;
String memberUID = "";
String memberName = "";
String bookUID = "";
String bookTitle = "";
uint8_t bookSlot = 0;
bool pir1Init = false;
bool pir2Init = false;
unsigned long stateTimer = 0;
unsigned long blinkTimer = 0;
bool blinkState = false;
// ══════════════════════════════════════════════════════════════
// FUNCTION PROTOTYPES
// ══════════════════════════════════════════════════════════════
void connectWiFi();
void syncNTP();
void initFirebase();
void fbSet(const String& path, const String& jsonObj);
String makeTimestamp();
void setState(State s);
void handleIdle();
void handleMenu();
void handleEmpruntPorte();
void handleEmpruntRetire();
void handleEmpruntAlerte();
void handleRetourScan();
void handleRetourPorte();
void handleRetourAlerte();
void handleRetourCountdown();
String readRFID();
const Member* findMember(const String& uid);
const Book* findBook (const String& uid);
void lcdPrint(const char* l1, const char* l2 = "");
void openDoor();
void closeDoor();
enum RGBColor { RGB_OFF, RGB_BLUE, RGB_GREEN, RGB_RED, RGB_ORANGE };
void setRGB(RGBColor c);
void beep(int ms = 120);
void longBeep();
bool btnPressed(int pin);
bool slotPresent(uint8_t slot);
// ══════════════════════════════════════════════════════════════
// SETUP
// ══════════════════════════════════════════════════════════════
void setup() {
Serial.begin(115200);
delay(300);
// ── GPIO ───────────────────────────────────────────────────
pinMode(PIN_RGB_R, OUTPUT);
pinMode(PIN_RGB_G, OUTPUT);
pinMode(PIN_RGB_B, OUTPUT);
pinMode(PIN_BUZZER, OUTPUT);
pinMode(PIN_PIR1, INPUT);
pinMode(PIN_PIR2, INPUT);
pinMode(PIN_BTN_EMPRUNT, INPUT_PULLUP);
pinMode(PIN_BTN_RESET, INPUT_PULLUP);
pinMode(PIN_BTN_RETOUR, INPUT_PULLUP);
// ── LCD ────────────────────────────────────────────────────
Wire.begin(PIN_LCD_SDA, PIN_LCD_SCL);
lcd.init();
lcd.backlight();
lcdPrint("Bibliotheque", "Intelligente");
// ── Servo ──────────────────────────────────────────────────
doorServo.attach(PIN_SERVO, 500, 2400);
closeDoor();
delay(500);
// ── RFID ───────────────────────────────────────────────────
SPI.begin(PIN_RFID_SCK, PIN_RFID_MISO, PIN_RFID_MOSI, PIN_RFID_SDA);
rfid.PCD_Init();
Serial.println("[RFID] RC522 initialisé");
// ── Network + Firebase ─────────────────────────────────────
connectWiFi(); // connects WiFi then calls syncNTP() internally
Serial.printf("[HEAP] Free heap: %d bytes\n", ESP.getFreeHeap());
initFirebase();
delay(1500);
setState(STATE_IDLE);
}
// ══════════════════════════════════════════════════════════════
// LOOP
// ══════════════════════════════════════════════════════════════
void loop() {
// ── Firebase maintenance (must run every loop, no blocking delay) ──
fbApp.loop();
Database.loop();
// Bind Database once authenticated (LegacyToken is almost instant)
if (!fbReady && fbApp.ready()) {
fbReady = true;
fbApp.getApp<RealtimeDatabase>(Database);
Database.url(FIREBASE_DB_URL);
Serial.println("[FB] Realtime Database prêt ✓");
fbSet("/bibliotheque/status/systemState",
"{\"etat\":\"idle\",\"ts\":\"" + makeTimestamp() + "\"}");
}
// ── RESET button — top priority in every non-idle state ────
if (currentState != STATE_IDLE && btnPressed(PIN_BTN_RESET)) {
Serial.println("[RESET] Bouton reset pressé");
beep(50); delay(60); beep(50);
setState(STATE_IDLE);
return;
}
// ── State machine ──────────────────────────────────────────
switch (currentState) {
case STATE_IDLE: handleIdle(); break;
case STATE_MENU: handleMenu(); break;
case STATE_EMPRUNT_PORTE: handleEmpruntPorte(); break;
case STATE_EMPRUNT_RETIRE: handleEmpruntRetire(); break;
case STATE_EMPRUNT_ALERTE: handleEmpruntAlerte(); break;
case STATE_RETOUR_SCAN: handleRetourScan(); break;
case STATE_RETOUR_PORTE: handleRetourPorte(); break;
case STATE_RETOUR_ALERTE: handleRetourAlerte(); break;
case STATE_RETOUR_COUNTDOWN: handleRetourCountdown(); break;
}
}
/**
* fbSet — asynchronous SET to Realtime Database.
* jsonObj must be a valid JSON string, e.g. "{\"key\":\"value\"}".
*/
void fbSet(const String& path, const String& jsonObj) {
if (!fbReady) {
Serial.println("[FB] Pas encore prêt — SET ignoré : " + path);
return;
}
Database.set<object_t>(aClient, path,
object_t(jsonObj.c_str()),
asyncCB,
"SET_" + path);
Serial.printf("[FB] SET queued → %s\n", path.c_str());
}
void fbPush(const String& path, const String& jsonObj) {
if (!fbReady) {
Serial.println("[FB] Pas encore prêt — PUSH ignoré : " + path);
return;
}
Database.push<object_t>(aClient, path,
object_t(jsonObj.c_str()),
asyncCB,
"PUSH_" + path);
Serial.printf("[FB] PUSH queued → %s\n", path.c_str());
}
// ══════════════════════════════════════════════════════════════
// STATE HANDLERS
// ══════════════════════════════════════════════════════════════
void handleIdle() {
String uid = readRFID();
if (uid.isEmpty()) return;
const Member* m = findMember(uid);
if (m) {
memberUID = uid;
memberName = String(m->name);
Serial.printf("[RFID] Membre reconnu : %s (%s)\n", m->name, uid.c_str());
setRGB(RGB_GREEN);
beep(100);
lcdPrint(("Bonjour " + memberName + "!").c_str());
delay(200);
fbPush("/bibliotheque/logs/scan_carte",
"{\"uid\":\"" + uid + "\","
"\"nom\":\"" + memberName + "\","
"\"action\":\"scan_carte\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/status/dernierMembre",
"{\"nom\":\"" + memberName + "\"}");
delay(1300);
setState(STATE_MENU);
} else {
Serial.printf("[RFID] Badge inconnu : %s\n", uid.c_str());
setRGB(RGB_RED);
lcdPrint("Acces refuse !", uid.c_str());
longBeep();
fbPush("/bibliotheque/logs/acces_refuse",
"{\"uid\":\"" + uid + "\","
"\"action\":\"acces_refuse\","
"\"ts\":\"" + makeTimestamp() + "\"}");
delay(2000);
setRGB(RGB_BLUE);
lcdPrint("Scannez carte", "membre...");
}
}
void handleMenu() {
if (millis() - stateTimer > TIMEOUT_MENU) {
lcdPrint("Timeout menu", "");
delay(800);
setState(STATE_IDLE);
return;
}
if (btnPressed(PIN_BTN_EMPRUNT)) {
Serial.println("[MENU] Emprunt choisi");
beep();
pir1Init = slotPresent(1);
pir2Init = slotPresent(2);
openDoor();
setState(STATE_EMPRUNT_PORTE);
}
else if (btnPressed(PIN_BTN_RETOUR)) {
Serial.println("[MENU] Retour choisi");
beep();
setState(STATE_RETOUR_SCAN);
}
}
void handleEmpruntPorte() {
// Timeout: close door and return to idle if no book is taken
if (millis() - stateTimer > TIMEOUT_SCAN_LIVRE) {
Serial.println("[EMPRUNT] Timeout porte — aucun livre retiré");
closeDoor();
lcdPrint("Aucun livre", "pris. Annule.");
delay(1500);
setState(STATE_IDLE);
return;
}
bool s1 = slotPresent(1);
bool s2 = slotPresent(2);
if (pir1Init && !s1) {
bookSlot = 1;
Serial.println("[EMPRUNT] Livre retiré — slot 1");
setState(STATE_EMPRUNT_RETIRE);
}
else if (pir2Init && !s2) {
bookSlot = 2;
Serial.println("[EMPRUNT] Livre retiré — slot 2");
setState(STATE_EMPRUNT_RETIRE);
}
}
void handleEmpruntRetire() {
if (millis() - stateTimer > TIMEOUT_SCAN_LIVRE) {
Serial.println("[EMPRUNT] Timeout scan → alerte vol");
fbPush("/bibliotheque/logs/alertes",
"{\"membre\":\"" + memberName + "\","
"\"slot\":" + String(bookSlot) + ","
"\"action\":\"alerte_vol\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/status/alerte", "{\"type\":\"alerte_vol\"}");
setState(STATE_EMPRUNT_ALERTE);
return;
}
if (slotPresent(bookSlot)) {
Serial.println("[EMPRUNT] Livre remis → annulation");
closeDoor();
lcdPrint("Emprunt annule", "");
delay(1500);
setState(STATE_IDLE);
return;
}
String uid = readRFID();
if (uid.isEmpty()) return;
const Book* b = findBook(uid);
if (b) {
bookUID = uid;
bookTitle = String(b->title);
Serial.printf("[EMPRUNT] Livre : %s (slot %d)\n", b->title, b->slot);
fbPush("/bibliotheque/logs/emprunts",
"{\"membre\":\"" + memberName + "\","
"\"membreUID\":\"" + memberUID + "\","
"\"livre\":\"" + bookTitle + "\","
"\"livreUID\":\"" + bookUID + "\","
"\"slot\":" + String(bookSlot) + ","
"\"action\":\"emprunt\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/livres/" + bookUID,
"{\"titre\":\"" + bookTitle + "\","
"\"statut\":\"emprunte\","
"\"empruntePar\":\"" + memberName + "\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/status/alerte", "{\"type\":\"aucune\"}");
setRGB(RGB_GREEN);
lcdPrint("Bonne lecture!", bookTitle.c_str());
beep(250);
delay(2000);
closeDoor();
setState(STATE_IDLE);
} else {
Serial.printf("[EMPRUNT] Livre inconnu : %s\n", uid.c_str());
setRGB(RGB_ORANGE);
lcdPrint("Livre inconnu!", "Rescannez...");
beep(400);
delay(1500);
setRGB(RGB_GREEN);
lcdPrint("Scannez le", "livre SVP !");
}
}
void handleEmpruntAlerte() {
if (millis() - blinkTimer > 400) {
blinkTimer = millis();
blinkState = !blinkState;
if (blinkState) {
setRGB(RGB_RED);
tone(PIN_BUZZER, 1000);
lcdPrint("!! ALERTE VOL !", "Scannez livre!");
} else {
setRGB(RGB_OFF);
noTone(PIN_BUZZER);
}
}
if (slotPresent(bookSlot)) {
noTone(PIN_BUZZER);
Serial.println("[ALERTE VOL] Livre remis sur étagère");
closeDoor();
fbSet("/bibliotheque/status/alerte", "{\"type\":\"aucune\"}");
lcdPrint("Livre repose", "Porte fermee");
delay(2000);
setState(STATE_IDLE);
return;
}
String uid = readRFID();
if (uid.isEmpty()) return;
const Book* b = findBook(uid);
if (b) {
noTone(PIN_BUZZER);
bookUID = uid;
bookTitle = String(b->title);
Serial.printf("[ALERTE VOL] Livre scanné : %s\n", b->title);
fbPush("/bibliotheque/logs/emprunts",
"{\"membre\":\"" + memberName + "\","
"\"livre\":\"" + bookTitle + "\","
"\"livreUID\":\"" + bookUID + "\","
"\"action\":\"emprunt_post_alerte\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/livres/" + bookUID,
"{\"titre\":\"" + bookTitle + "\","
"\"statut\":\"emprunte\","
"\"empruntePar\":\"" + memberName + "\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/status/alerte", "{\"type\":\"aucune\"}");
setRGB(RGB_GREEN);
lcdPrint("Bonne lecture!", bookTitle.c_str());
beep(250);
delay(2000);
closeDoor();
setState(STATE_IDLE);
}
}
void handleRetourScan() {
if (millis() - stateTimer > TIMEOUT_MENU) {
lcdPrint("Timeout retour", "");
delay(800);
setState(STATE_IDLE);
return;
}
String uid = readRFID();
if (uid.isEmpty()) return;
const Book* b = findBook(uid);
if (b) {
bookUID = uid;
bookSlot = b->slot;
bookTitle = String(b->title);
Serial.printf("[RETOUR] Livre : %s slot %d\n", b->title, b->slot);
beep(100);
lcdPrint(("Deposez slot " + String(bookSlot)).c_str(),
bookTitle.c_str());
delay(1500);
openDoor();
setState(STATE_RETOUR_PORTE);
} else {
Serial.printf("[RETOUR] Livre inconnu : %s\n", uid.c_str());
setRGB(RGB_ORANGE);
lcdPrint("Livre inconnu!", "Rescannez...");
beep(400);
delay(1500);
setRGB(RGB_GREEN);
lcdPrint("Retour livre", "Scannez livre");
}
}
void handleRetourPorte() {
if (slotPresent(bookSlot)) {
Serial.println("[RETOUR] Livre détecté → countdown 5s");
setState(STATE_RETOUR_COUNTDOWN);
return;
}
if (millis() - stateTimer > TIMEOUT_DEPOT_LIVRE) {
Serial.println("[RETOUR] Timeout → alerte non-retour");
fbPush("/bibliotheque/logs/alertes",
"{\"membre\":\"" + memberName + "\","
"\"livre\":\"" + bookTitle + "\","
"\"slot\":" + String(bookSlot) + ","
"\"action\":\"alerte_non_retour\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/status/alerte", "{\"type\":\"alerte_non_retour\"}");
setState(STATE_RETOUR_ALERTE);
}
}
void handleRetourAlerte() {
if (millis() - blinkTimer > 400) {
blinkTimer = millis();
blinkState = !blinkState;
if (blinkState) {
setRGB(RGB_RED);
tone(PIN_BUZZER, 900);
lcdPrint("Livre manquant!", "Deposez-le !");
} else {
setRGB(RGB_OFF);
noTone(PIN_BUZZER);
}
}
if (slotPresent(bookSlot)) {
noTone(PIN_BUZZER);
Serial.println("[RETOUR ALERTE] Livre détecté → countdown");
setState(STATE_RETOUR_COUNTDOWN);
}
}
void handleRetourCountdown() {
if (!slotPresent(bookSlot)) {
Serial.println("[COUNTDOWN] Livre retiré → alerte non-retour");
setState(STATE_RETOUR_ALERTE);
return;
}
unsigned long elapsed = millis() - stateTimer;
int remaining = (int)((COUNTDOWN_FERMETURE - (long)elapsed) / 1000) + 1;
if (remaining < 0) remaining = 0;
static int lastShown = -1;
if (remaining != lastShown) {
lastShown = remaining;
lcdPrint(("Fermeture " + String(remaining) + "s..").c_str(),
"Retirez main");
}
if (elapsed >= COUNTDOWN_FERMETURE) {
lastShown = -1;
Serial.println("[RETOUR] Retour validé → fermeture porte");
fbPush("/bibliotheque/logs/retours",
"{\"membre\":\"" + memberName + "\","
"\"membreUID\":\"" + memberUID + "\","
"\"livre\":\"" + bookTitle + "\","
"\"livreUID\":\"" + bookUID + "\","
"\"slot\":" + String(bookSlot) + ","
"\"action\":\"retour\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/livres/" + bookUID,
"{\"titre\":\"" + bookTitle + "\","
"\"statut\":\"disponible\","
"\"empruntePar\":\"\","
"\"ts\":\"" + makeTimestamp() + "\"}");
fbSet("/bibliotheque/status/alerte", "{\"type\":\"aucune\"}");
closeDoor();
setRGB(RGB_GREEN);
lcdPrint("Retour enreg.!", bookTitle.c_str());
beep(300);
delay(2000);
setState(STATE_IDLE);
}
}
// ══════════════════════════════════════════════════════════════
// setState — central transition handler
// ══════════════════════════════════════════════════════════════
void setState(State s) {
currentState = s;
stateTimer = millis();
blinkState = false;
noTone(PIN_BUZZER);
switch (s) {
case STATE_IDLE:
memberUID = ""; memberName = "";
bookUID = ""; bookTitle = ""; bookSlot = 0;
closeDoor();
setRGB(RGB_BLUE);
lcdPrint("Scannez carte", "membre...");
break;
case STATE_MENU:
setRGB(RGB_GREEN);
lcdPrint("1=Emprunt", "3=Retour");
break;
case STATE_EMPRUNT_PORTE:
setRGB(RGB_GREEN);
lcdPrint("Porte ouverte", "Prenez un livre");
break;
case STATE_EMPRUNT_RETIRE:
setRGB(RGB_GREEN);
lcdPrint("Scannez le", "livre SVP !");
break;
case STATE_EMPRUNT_ALERTE:
break; // handled by handleEmpruntAlerte() blink logic
case STATE_RETOUR_SCAN:
setRGB(RGB_GREEN);
lcdPrint("Retour livre", "Scannez livre");
break;
case STATE_RETOUR_PORTE:
setRGB(RGB_GREEN);
lcdPrint(("Deposez slot " + String(bookSlot)).c_str(),
bookTitle.c_str());
break;
case STATE_RETOUR_ALERTE:
break; // handled by handleRetourAlerte() blink logic
case STATE_RETOUR_COUNTDOWN:
setRGB(RGB_GREEN);
lcdPrint("Fermeture 5s..", "Retirez main");
break;
}
}
// ══════════════════════════════════════════════════════════════
// FIREBASE HELPERS
// ══════════════════════════════════════════════════════════════
/**
* asyncCB — FirebaseClient v2.x universal async callback.
*/
void asyncCB(AsyncResult& aResult) {
if (aResult.appEvent().code() > 0) {
Firebase.printf("[FB][Auth] %s (code %d)\n",
aResult.appEvent().message().c_str(),
aResult.appEvent().code());
}
if (aResult.isDebug()) {
Firebase.printf("[FB][Dbg] %s\n", aResult.debug().c_str());
}
if (aResult.isError()) {
Firebase.printf("[FB][Err] %s (code %d)\n",
aResult.error().message().c_str(),
aResult.error().code());
}
if (aResult.available()) {
Firebase.printf("[FB][Data] %s\n", aResult.c_str());
}
}
// ══════════════════════════════════════════════════════════════
// WIFI + NTP + FIREBASE INIT
// ══════════════════════════════════════════════════════════════
void syncNTP() {
Serial.println("[TIME] Starting NTP sync...");
// configTzTime sets timezone AND starts NTP — Algeria UTC+1 no DST
configTzTime(NTP_TZ, NTP_SERVER);
struct tm timeinfo;
Serial.print("[TIME] Waiting");
for (int i = 0; i < 40; i++) {
if (getLocalTime(&timeinfo)) {
Serial.printf("\n[TIME] OK: %04d-%02d-%02d %02d:%02d:%02d (Algeria UTC+1)\n",
timeinfo.tm_year + 1900,
timeinfo.tm_mon + 1,
timeinfo.tm_mday,
timeinfo.tm_hour,
timeinfo.tm_min,
timeinfo.tm_sec);
return;
}
delay(500);
Serial.print(".");
}
Serial.println("\n[TIME] NTP sync failed — timestamps will use uptime fallback");
}
void connectWiFi() {
Serial.printf("[WiFi] Connexion à %s\n", WIFI_SSID);
lcdPrint("Connexion WiFi", WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
unsigned long t = millis();
while (WiFi.status() != WL_CONNECTED && millis() - t < 20000UL) {
delay(500);
Serial.print(".");
}
Serial.println();
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("[WiFi] Connecté — IP : %s\n",
WiFi.localIP().toString().c_str());
lcdPrint("WiFi OK", WiFi.localIP().toString().c_str());
delay(500);
// NTP sync must happen after WiFi is up and before Firebase init
syncNTP();
} else {
Serial.println("[WiFi] Echec connexion — mode local");
lcdPrint("WiFi ECHEC", "Mode local");
}
delay(800);
}
void initFirebase() {
lcdPrint("Firebase", "connexion...");
// setInsecure() skips SSL certificate verification — required for Wokwi
ssl_client.setInsecure();
ssl_client.setHandshakeTimeout(30); // 30 sec — critical for Wokwi
// LegacyToken auth: no identitytoolkit.googleapis.com handshake needed,
// token is appended as ?auth=<secret> on each REST call → much faster
initializeApp(aClient, fbApp, getAuth(legacy_token), asyncCB, "authTask");
// Bind Database service to the app
fbApp.getApp<RealtimeDatabase>(Database);
Database.url(FIREBASE_DB_URL);
Serial.println("[FB] Initialisation LegacyToken — en attente ready...");
}
// ══════════════════════════════════════════════════════════════
// HARDWARE HELPERS
// ══════════════════════════════════════════════════════════════
String readRFID() {
if (!rfid.PICC_IsNewCardPresent() || !rfid.PICC_ReadCardSerial()) {
return "";
}
String uid = "";
for (byte i = 0; i < rfid.uid.size; i++) {
if (i > 0) uid += ":";
if (rfid.uid.uidByte[i] < 0x10) uid += "0";
uid += String(rfid.uid.uidByte[i], HEX);
}
uid.toUpperCase();
rfid.PICC_HaltA();
rfid.PCD_StopCrypto1();
Serial.printf("[RFID] UID lu : %s\n", uid.c_str());
return uid;
}
const Member* findMember(const String& uid) {
for (uint8_t i = 0; i < MEMBER_COUNT; i++)
if (uid == String(MEMBERS[i].uid)) return &MEMBERS[i];
return nullptr;
}
const Book* findBook(const String& uid) {
for (uint8_t i = 0; i < BOOK_COUNT; i++)
if (uid == String(BOOKS[i].uid)) return &BOOKS[i];
return nullptr;
}
void lcdPrint(const char* l1, const char* l2) {
lcd.clear();
lcd.setCursor(0, 0);
lcd.print(l1);
if (l2 && l2[0] != '\0') {
lcd.setCursor(0, 1);
lcd.print(l2);
}
}
void openDoor() { doorServo.write(SERVO_OPEN); Serial.println("[SERVO] Ouvert 90°"); }
void closeDoor() { doorServo.write(SERVO_CLOSED); Serial.println("[SERVO] Ferme 0°"); }
void setRGB(RGBColor c) {
bool r = false, g = false, b = false;
switch (c) {
case RGB_OFF: break;
case RGB_BLUE: b = true; break;
case RGB_GREEN: g = true; break;
case RGB_RED: r = true; break;
case RGB_ORANGE: r = true; g = true; break;
}
digitalWrite(PIN_RGB_R, r ? HIGH : LOW);
digitalWrite(PIN_RGB_G, g ? HIGH : LOW);
digitalWrite(PIN_RGB_B, b ? HIGH : LOW);
}
void beep(int ms) {
tone(PIN_BUZZER, 1000, ms);
delay(ms + 10);
}
void longBeep() {
tone(PIN_BUZZER, 700, 800);
delay(820);
}
bool btnPressed(int pin) {
if (digitalRead(pin) == LOW) {
delay(30);
if (digitalRead(pin) == LOW) {
while (digitalRead(pin) == LOW) delay(5);
return true;
}
}
return false;
}
bool slotPresent(uint8_t slot) {
if (slot == 1) return digitalRead(PIN_PIR1) == HIGH;
if (slot == 2) return digitalRead(PIN_PIR2) == HIGH;
return false;
}
String makeTimestamp() {
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
// Fallback to uptime if NTP not yet synced
unsigned long s = millis() / 1000;
char buf[12];
snprintf(buf, sizeof(buf), "%02lu:%02lu:%02lu",
(s / 3600) % 24, (s / 60) % 60, s % 60);
return String(buf);
}
char buf[30];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &timeinfo);
return String(buf);
}