Skip to content

Project Development

This section documents the complete development process of my final project — from the initial sketch to the assembly and testing of the finished system.

The project is a Smart Access Control System for the FabLab, which grants entry only to registered members via an RFID badge and logs all entries and exits through a web interface.


01. Sketch and System Architecture

The project was designed around two main functional units:

  • The Access Module — mounted at the FabLab entrance, it houses the RFID reader, buzzer, and LED indicators.

  • The Door Control Module — mounted inside the FabLab, it houses the relay and the electrically controlled door lock.

Below is the initial concept sketch of the system:

Project sketch

The diagram below illustrates the overall architecture of the system:

System architecture

System architecture


02. Design of the Enclosure

For the 3D design of the device enclosure, I used SolidWorks.

Outdoor Access Module

The enclosure is made up of two parts:

  • The main shell — houses the ESP32 PCB, the RFID reader, LCD screen, LEDs, and buzzer
  • The cover — closes the enclosure

Enclosure 3D design

Enclosure 3D design

Design files:

Indoor Door Control Module

The enclosure is made up of two parts:

  • The main shell — houses the ESP8266 and the relay module
  • The cover — closes the enclosure

Indoor enclosure 3D design

Design files:


03. 3D Printing

The enclosure was printed using the Prusa MK4S+ available in our FabLab.

Printing parameters used:

Parameter Value
Print setting 0.20mm QUALITY
Filament PLA
Supports Used on the main shell only
Infill 20%

For more details on the 3D printing procedure, refer to Week 5 — 3D Scanning and Printing.

Outdoor Access Module

Printed outdoor enclosure

Indoor Door Control Module

Printed indoor enclosure


04. Laser Cutting

For the cover of the enclosure, I also explored a laser-cut version using 3mm MDF on the Epilog Laser available in our FabLab.

For more details on the laser cutting procedure, refer to Week 3 — Computer Controlled Cutting.

Laser cut panel

Laser cut panel

Design file:


05. Electronics Design

For the electronic design of the system, I used KiCad.

Outdoor Access Module

The circuit integrates the following components on a single custom PCB:

  • ESP32 microcontroller
  • RFID RC522 reader (SPI communication)
  • TFT LCD ILI9488 screen
  • Buzzer and LED indicators
  • Push button

For more details on the electronics design process, refer to Week 6 — Electronics Design.

Below is the schematic and PCB layout of the circuit:

Circuit schematic

Circuit schematic

PCB layout

PCB layout

PCB 3D view

PCB 3D view

Design files:


Indoor Door Control Module

The diagram below shows how all five components of the interior door control module are connected together:

Interior door module wiring diagram

Interior door module wiring diagram

Wire Summary

# From Pin To Pin Wire color Voltage
1 Power Supply VCC Module 2PHM6602A VCC Red +12V
2 Power Supply GND Module 2PHM6602A GND Black 0V
3 Module 2PHM6602A 5V ESP-01S Relay 5V Green +5V
4 Module 2PHM6602A 0V ESP-01S Relay 0V Black 0V
5 Power Supply VCC ESP-01S Relay COM Red +12V
6 ESP-01S Relay NC Push Button (NC) Terminal 1 Black +12V switched
7 Push Button (NC) Terminal 2 12V Door Lock VCC Red +12V switched
8 Power Supply GND 12V Door Lock GND Black 0V

1. ESP-01S Relay Module V1.0

ESP-01S Relay Module

The ESP-01S Relay Module V1.0 is a compact Wi-Fi controlled relay board built around the AI-Thinker ESP-01S (ESP8266-based) Wi-Fi module. It integrates the ESP-01S directly onto the board via an 8-pin female header, making it a self-contained smart switch solution. The relay is controlled via GPIO0 of the ESP8266.

This module was chosen because it combines Wi-Fi connectivity and relay switching in a single compact form factor, which is ideal for remotely controlling the door lock in our access control system.

Key Specifications:

Parameter Value
Operating Voltage DC 5V
Wi-Fi Module ESP-01S (ESP8266)
Relay Type Mechanical (Songle SRD-05VDC-SL-C)
Relay Load Capacity 10A / 250VAC — 10A / 30VDC
Control Pin GPIO0 (active LOW)
Voltage Regulator AMS1117 3.3V (for ESP-01S)
Dimensions 36 × 25 × 16 mm

Wiring:

Pin Description
VCC +5V DC power input
GND Ground
NO Normally Open relay contact
COM Common relay contact
NC Normally Closed relay contact

🔗 Product page 🔗 GitHub — ESP-01S Relay V1.0 schematic and demo code


2. Mini MP1584 DC-DC Step-Down Buck Converter

MP1584 Buck Converter

The Mini MP1584 is a small-form-factor adjustable step-down (buck) DC-DC converter module based on the MP1584 high-frequency switching regulator IC. The output voltage is adjusted via an onboard potentiometer — rotating clockwise increases the output voltage, and counter-clockwise decreases it.

In this project, the MP1584 is used to step down the 12V power supply to 5V for the ESP-01S Relay Module.

Key Specifications:

Parameter Value
Input Voltage 4.5V – 28V DC
Output Voltage 0.8V – 20V DC (adjustable)
Maximum Output Current 3A
Module Size Small form factor (approx. 22 × 17 mm)

Pin Configuration:

Pin Description
IN+ Positive input voltage terminal
IN− Negative input voltage terminal
OUT+ Positive output voltage terminal
OUT− Negative output voltage terminal

🔗 Component page — Components101 🔗 MP1584 Datasheet


3. 12V Electric Mortise Door Lock (Fail-Safe NC)

Electric Mortise Lock

The HFeng 12V Electric Drop Bolt Mortise Lock is an electromagnetic door lock designed for access control systems. It operates in Fail-Safe (NC) mode, meaning the door is locked when power is on and unlocked when power is cut off. This ensures the door automatically opens in the event of a power failure — an important safety requirement for public-access environments like a FabLab.

It features an adjustable time delay (0, 3, or 6 seconds) before the bolt re-engages after an unlock command.

Key Specifications:

Parameter Value
Operating Voltage DC 12V
Lock Mode Magnetic induction (drop bolt)
Security Mode Fail-Safe / NC (locked when powered)
Startup Current 1.2A
Operating Current 0.25A
Dimensions 150 × 34 × 28 mm
Time Delay Adjustable: 0 / 3 / 6 seconds

Wiring:

Wire Color Function
Red +12V DC
Black GND
Yellow NC (Normally Closed signal)
White COM (Common)

🔗 Amazon product page


4. Push Button — Siemens SIRIUS ACT 3SU1150-0AB20-1CA0

Siemens Push Button

This button is used in our system as the interior manual release button, allowing someone inside the FabLab to unlock the door without needing an RFID badge.

Key Specifications:

Parameter Value
Series Siemens SIRIUS ACT 3SU1
Reference 3SU1150-0AB20-1CA0
Mounting Diameter 22.3 mm
Contact Configuration 1 × NC (Normally Closed)
Connection Screw terminals
Mounting Panel mount, front mounting
Max. Operating Voltage 500V AC / 500V DC

🔗 RS Components


5. 12V 5A DC Power Supply (60W)

12V 5A Power Supply

The 12V 5A DC Power Supply is an AC-to-DC switching power adapter that provides the main power for the entire interior door control module. It converts 110–240V AC mains power to a stable 12V DC / 5A output, delivering up to 60W of continuous power.

The 12V output powers the electric door lock directly, while the MP1584 buck converter steps it down to 5V for the ESP-01S relay module. This single power supply is therefore sufficient to power the entire door control sub-system.

Key Specifications:

Parameter Value
Input Voltage AC 100–240V, 50/60Hz
Output Voltage DC 12V
Output Current 5A max
Output Power 60W
Enclosure Aluminium
Certifications CE, RoHS
Adjustable Output Yes — trimmer to adjust between ~10V and 15V

🔗 Product page — Leroy Merlin


06. Electronics Production

Outdoor Access Module

The PCB was fabricated using the Roland SRM-20 milling machine in our FabLab.

Tools used:

Tool Use
1/64 inch flat end mill Milling the circuit traces
1/32 inch flat end mill Cutting the board outline

For the full milling and soldering procedure, refer to Week 8 — Electronics Production.

Finished PCB

Indoor Door Control Module

Assembled indoor module


07. Programming and Network Configuration

For programming and network configuration of the modules, please refer to Week 11 — Networking and Communications.

Overall Network Architecture

The system is built around 4 main layers: Physical Edge Devices, Local Area Network (LAN), Cloud Database (Backend), and Web Supervision (Client).

Network architecture diagram


Communication Flow Timeline (Sequence Diagram)

The diagram below shows the complete path of a data frame, from the physical detection of the RFID badge to its real-time update on the web supervisor screen:

Communication sequence diagram


Communication Protocol Details

Link Protocol Physical Layer
ESP32 → ESP-01S ESP-NOW Wi-Fi 2.4 GHz Direct (peer-to-peer)
ESP32 → Supabase HTTPS (REST) Wi-Fi Station → Internet WAN
Web → Supabase WebSockets (WSS) LAN/WAN TCP Network
OTA (firmware updates) ElegantOTA (HTTP) Local LAN Network (Port 80)

08. Interface Programming

We chose to develop a web application using Next.js, which connects to a Supabase database to retrieve the access logs sent by the ESP32.

Setting Up the Supabase Database

Step 1 — Create the Supabase Account and Project

  1. Go to supabase.com and click Start your project.

  2. Sign in with GitHub (recommended) or create an email account.

  3. Click New project. Give it a name, e.g. rfid-access. Choose the closest region.

  4. Generate a strong password for the database (click Generate) and save it somewhere safe — you will not be able to see it again.

  5. Click Create new project and wait approximately 2 minutes for the project to be provisioned.

Supabase project creation

Retrieve Your API Keys

  1. In the left menu, click Project Settings (gear icon), then API.

  2. Note these two values — you will need them in the Arduino code:

Project URL  → https://XXXXXXXXXXXX.supabase.co
anon public  → eyJhbGciOiJIUzI1NiIsInR5cCI6Ikp...

Supabase API keys

⚠️ Important: Never share the service_role key — it grants full access to the database. Only use the anon key on the ESP32.


Step 2 — Create the Database Table

  1. In the left menu, click SQL Editor, then New query.

  2. Paste the following SQL and click Run (or Ctrl+Enter):

CREATE TABLE access_logs (
  id         uuid DEFAULT gen_random_uuid() PRIMARY KEY,
  uid_badge  text NOT NULL,
  statut     text CHECK (statut IN ('accorde','refuse')),
  lieu       text DEFAULT 'Porte principale',
  created_at timestamptz DEFAULT now()
);

Supabase SQL editor

  1. Enable Row Level Security and allow write access from the ESP32 (anon key):
ALTER TABLE access_logs ENABLE ROW LEVEL SECURITY;

CREATE POLICY "insert_depuis_esp32"
ON access_logs FOR INSERT
TO anon
WITH CHECK (true);

CREATE POLICY "lecture_admin"
ON access_logs FOR SELECT
TO anon
USING (true);

✅ In the Table Editor, you should now see the access_logs table with its 5 columns.

Supabase table view


Web Application Programming

The web application was built with Next.js.

Here is the result:

Web dashboard

1. Installing the Required Tools

To run this web application (Next.js + React) locally on your computer, you need to install Node.js.

Node.js is the JavaScript runtime that runs the Next.js server locally. NPM is the package manager that installs dependencies such as the Supabase client.

  1. Go to the official Node.js website: https://nodejs.org/
  2. Download the LTS version.
  3. Run the downloaded .msi installer and follow the setup wizard (leave all default options checked).

2. Configuring the Web Application

Open the file app-web/.env.local and replace the placeholder values with your actual Supabase credentials:

# Your project URL (available in Supabase → Settings → API)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co

# Your public anonymous key (anon public, available in Supabase → Settings → API)
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-public-key

3. Running and Testing Locally

Once your tools are installed and Supabase is configured, you can run the application locally and simulate badge scans to validate the full system.

Step 1 — Install NPM dependencies

Open a terminal, navigate to the application folder, and install the required modules (Next.js, Supabase):

cd app-web
npm install

Step 2 — Start the development server

Run the following command to start the application locally:

npm run dev

The terminal will confirm that the server is running at: http://localhost:3000

Step 3 — Open the application

Open your browser and go to http://localhost:3000.

Web dashboard

Source files:


09. Firmware — System Programming

Program 1 — Main Controller (ESP32)

Overview

The ESP32 is the brain of the system. It runs the following tasks simultaneously:

  • Reads RFID badges via the RC522 reader (SPI bus)
  • Displays real-time system logs on an ILI9341 TFT screen (HSPI bus)
  • Controls three status LEDs (red, blue, orange)
  • Connects to Wi-Fi and logs access events to a Supabase database via HTTPS
  • Sends wireless commands to the ESP-01S relay module via ESP-NOW
  • Exposes an OTA (Over-The-Air) firmware update endpoint via ElegantOTA

Libraries Used

Library Role
WiFi.h Wi-Fi connection management
esp_now.h ESP-NOW peer-to-peer wireless protocol
SPI.h SPI bus communication
MFRC522.h RFID RC522 reader driver
Adafruit_GFX.h Graphics primitives for the TFT screen
Adafruit_ILI9341.h ILI9341 TFT display driver
ESPAsyncWebServer.h Asynchronous HTTP web server
AsyncTCP.h Asynchronous TCP support for ESP32
ElegantOTA.h Web-based OTA firmware update interface
HTTPClient.h HTTP client for Supabase POST requests
WiFiClientSecure.h HTTPS (TLS) client for secure connections

Pin Configuration

RFID RC522 — Main SPI bus (custom GPIO reassignment)

Signal GPIO
SS (Chip Select) 5
RST 21
SCK 18
MISO 27
MOSI 23

ILI9341 TFT Screen — HSPI bus (dedicated secondary SPI)

Signal GPIO
CS 15
DC 2
RST 4
CLK 14
MOSI 13
MISO 19

LEDs

LED GPIO
Red (access denied) 26
Blue (standby / ready) 16
Orange (processing) 17

Note: The RFID and TFT screen use two separate SPI buses to avoid conflicts. The RFID uses the main SPI object with custom GPIO pins, while the TFT uses the SPIClass spiTFT(HSPI) object on the dedicated HSPI hardware bus.

Authorized Badges

Authorized RFID badge UIDs are stored directly in flash memory as byte arrays:

const byte NB_BADGES = 2;
byte badges_autorises[NB_BADGES][4] = {
  {0xD9, 0x16, 0xC6, 0x5A},
  {0xAB, 0xCD, 0xEF, 0x01}
};

To add a new badge, simply add its 4-byte UID to this array and increment NB_BADGES. The UID of any badge can be read by scanning it and observing the [RFID] log line on the TFT screen or the Serial monitor.

ESP-NOW Communication

ESP-NOW is used to send a wireless command from the ESP32 to the ESP-01S relay module without going through a router. The two devices communicate directly, peer-to-peer.

The message structure is defined as:

typedef struct { char commande[10]; } Message;

When an authorized badge is detected, the ESP32 sends:

strcpy(msg.commande, "OPEN");
esp_now_send(macESP01, (uint8_t *)&msg, sizeof(msg));

The MAC address of the ESP-01S must be entered manually in the macESP01 array. This address is printed to the Serial monitor when Program 2 boots.

Supabase Cloud Logging

Every badge scan — whether authorized or denied — is logged to a Supabase PostgreSQL database via an HTTPS POST request. The function sendLogToSupabase() builds a JSON payload and sends it to the REST API endpoint:

String jsonPayload = "{\"uid_badge\":\"" + uid + "\",\"statut\":\"" + statut + "\"}";
http.POST(jsonPayload);

The statut field is either "accorde" (granted) or "refuse" (denied).

WiFiClientSecure is used with client.setInsecure() to bypass strict certificate verification — acceptable for a prototype.

TFT Screen Display

The screen is divided into two zones:

  • Header (top 52px): Shows the system title, MAC address, and IP address. Updated once after Wi-Fi connects.
  • Log area (below 60px): Scrolling list of up to 10 log lines. Each new log line is color-coded:
  • 🟢 Green — authorized access, successful operations
  • 🔴 Red — denied access, errors
  • 🟠 Orange — in-progress operations (OTA, Supabase sending)
  • White — general system messages

When a badge is scanned, a colored flash bar appears between the header and the log area for 600ms as an immediate visual indicator.

OTA Updates

The firmware can be updated wirelessly via the ElegantOTA web interface, accessible at:

http://<ESP32_IP>/update

Setup Sequence

1. Initialize LEDs → orange (starting)
2. Initialize TFT screen → display "Demarrage..."
3. Initialize RFID reader
4. Connect to Wi-Fi
5. Draw header with IP and MAC address
6. Initialize ESP-NOW → register peer (ESP-01S MAC)
7. Start OTA web server
8. Set LED → blue (ready)
9. Display "Attente badge..."

Main Loop Logic

Loop:
├── ElegantOTA.loop()          ← handles OTA requests
└── RFID scan check
    ├── No card present → skip
    └── Card detected:
        ├── Read UID → format as hex string (e.g. "D9:16:C6:5A")
        ├── Log UID to screen
        ├── Check against authorized list
        │   ├── AUTHORIZED:
        │   │   ├── Log "Acces autorise" (green)
        │   │   ├── Flash green bar on screen
        │   │   ├── LED → orange
        │   │   ├── Send "OPEN" via ESP-NOW to ESP-01S
        │   │   ├── Wait 500ms
        │   │   ├── LED → blue
        │   │   └── Log to Supabase (statut: "accorde")
        │   └── DENIED:
        │       ├── Log "Acces refuse" (red)
        │       ├── Flash red bar on screen
        │       ├── LED → red
        │       ├── Wait 2000ms
        │       ├── LED → blue
        │       └── Log to Supabase (statut: "refuse")
        ├── Log "Attente badge..."
        ├── rfid.PICC_HaltA()         ← stop communication with card
        ├── rfid.PCD_StopCrypto1()    ← reset RFID crypto
        └── delay(1000)               ← debounce

Complete Code — Program 1

main_controller_esp32.ino
#include <WiFi.h>
#include <esp_now.h>
#include <SPI.h>
#include <MFRC522.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <ESPAsyncWebServer.h>
#include <AsyncTCP.h>
#include <ElegantOTA.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>

// ── WiFi ───────────────────────────────────────────────
const char* ssid         = "WiFi INPHB NEWS";
const char* ota_username = "mpass";
const char* ota_password = "25vfvft56]@224044";

// ── Supabase ───────────────────────────────────────────
const char* supabase_url = "https://kuliglh.supabase.co/rest/v1/access_logs";
const char* supabase_key = "eyJhbGcIkpXVCJ9..."; // truncated

// ── Pins RFID (main SPI bus with custom GPIO) ──────────
#define RFID_SS    5
#define RFID_RST   21
#define RFID_SCK   18
#define RFID_MISO  27
#define RFID_MOSI  23

// ── Pins ILI9341 (HSPI dedicated bus) ─────────────────
#define TFT_CS    15
#define TFT_DC     2
#define TFT_RST    4
#define TFT_CLK   14
#define TFT_MOSI  13
#define TFT_MISO  19

// ── LED Pins ───────────────────────────────────────────
#define LED_ROUGE   26
#define LED_BLEUE   16
#define LED_ORANGE  17

// ── ESP-01S MAC address ────────────────────────────────
uint8_t macESP01[] = {0xA4, 0xE5, 0x7C, 0xB6, 0x1D, 0x52};

// ── Authorized badge UIDs ──────────────────────────────
const byte NB_BADGES = 2;
byte badges_autorises[NB_BADGES][4] = {
  {0xD9, 0x16, 0xC6, 0x5A},
  {0xAB, 0xCD, 0xEF, 0x01}
};

// ── Objects ────────────────────────────────────────────
MFRC522          rfid(RFID_SS, RFID_RST);
SPIClass         spiTFT(HSPI);
Adafruit_ILI9341 tft(&spiTFT, TFT_DC, TFT_CS, TFT_RST);
AsyncWebServer   server(80);

// ── ESP-NOW message structure ──────────────────────────
typedef struct { char commande[10]; } Message;
Message msg;

// ── Screen log buffer ──────────────────────────────────
#define MAX_LOGS     10
#define LOG_Y_START  60
#define LOG_LINE_H   16
#define SCREEN_W    320
#define SCREEN_H    240

String logs[MAX_LOGS];
int    logCount = 0;

#define COL_BG     ILI9341_BLACK
#define COL_HEADER ILI9341_WHITE
#define COL_GREEN  ILI9341_GREEN
#define COL_RED    ILI9341_RED
#define COL_ORANGE 0xFD20
#define COL_GREY   0x8410
#define COL_WHITE  ILI9341_WHITE

void drawHeader() {
  tft.fillRect(0, 0, SCREEN_W, 52, COL_HEADER);
  tft.setTextColor(ILI9341_BLACK);
  tft.setTextSize(2);
  tft.setCursor(8, 6);
  tft.print("INPHB  SERRURE");
  tft.setTextSize(1);
  tft.setCursor(8, 30);
  tft.print("MAC: " + WiFi.macAddress());
  tft.setCursor(8, 42);
  tft.print("IP : " + WiFi.localIP().toString());
  tft.drawFastHLine(0, 52, SCREEN_W, COL_ORANGE);
}

void addLog(String texte, uint16_t couleur = COL_WHITE) {
  Serial.println(texte);
  if (logCount >= MAX_LOGS) {
    for (int i = 0; i < MAX_LOGS - 1; i++) logs[i] = logs[i + 1];
    logCount = MAX_LOGS - 1;
  }
  logs[logCount++] = texte;
  tft.fillRect(0, LOG_Y_START, SCREEN_W, SCREEN_H - LOG_Y_START, COL_BG);
  for (int i = 0; i < logCount; i++) {
    uint16_t c = COL_WHITE;
    if (logs[i].indexOf("autorise") >= 0 || logs[i].indexOf("OK") >= 0)    c = COL_GREEN;
    if (logs[i].indexOf("refuse")  >= 0 || logs[i].indexOf("Erreur") >= 0) c = COL_RED;
    if (logs[i].indexOf("OTA")     >= 0 || logs[i].indexOf("...") >= 0)    c = COL_ORANGE;
    tft.setTextColor(c);
    tft.setTextSize(1);
    tft.setCursor(4, LOG_Y_START + i * LOG_LINE_H);
    tft.print(logs[i]);
  }
}

void showBadgeFlash(bool autorise) {
  uint16_t col = autorise ? COL_GREEN : COL_RED;
  tft.fillRect(0, 53, SCREEN_W, 6, col);
  delay(600);
  tft.fillRect(0, 53, SCREEN_W, 6, COL_BG);
}

void setLED(int pin) {
  digitalWrite(LED_ROUGE,  pin == LED_ROUGE  ? HIGH : LOW);
  digitalWrite(LED_BLEUE,  pin == LED_BLEUE  ? HIGH : LOW);
  digitalWrite(LED_ORANGE, pin == LED_ORANGE ? HIGH : LOW);
}

bool badge_autorise(byte *uid, byte size) {
  if (size != 4) return false;
  for (int i = 0; i < NB_BADGES; i++)
    if (memcmp(uid, badges_autorises[i], 4) == 0) return true;
  return false;
}

void onDataSent(const esp_now_send_info_t *info, esp_now_send_status_t status) {
  bool ok = (status == ESP_NOW_SEND_SUCCESS);
  addLog(ok ? "[ESP-NOW] Envoi OK" : "[ESP-NOW] Envoi ECHEC",
         ok ? COL_GREEN : COL_RED);
}

void sendLogToSupabase(String uid, String statut) {
  if (WiFi.status() != WL_CONNECTED) {
    addLog("[Supabase] Non connecte", COL_RED);
    return;
  }
  WiFiClientSecure client;
  client.setInsecure();
  HTTPClient http;
  if (http.begin(client, supabase_url)) {
    http.addHeader("Content-Type", "application/json");
    http.addHeader("apikey", supabase_key);
    http.addHeader("Authorization", "Bearer " + String(supabase_key));
    http.addHeader("Prefer", "return=minimal");
    String jsonPayload = "{\"uid_badge\":\"" + uid + "\",\"statut\":\"" + statut + "\"}";
    addLog("[Supabase] Envoi log...", COL_ORANGE);
    int code = http.POST(jsonPayload);
    if (code >= 200 && code < 300) addLog("[Supabase] Envoi OK", COL_GREEN);
    else addLog("[Supabase] HTTP: " + String(code), COL_RED);
    http.end();
  } else {
    addLog("[Supabase] Erreur init client", COL_RED);
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(LED_ROUGE, OUTPUT);
  pinMode(LED_BLEUE, OUTPUT);
  pinMode(LED_ORANGE, OUTPUT);
  setLED(LED_ORANGE);

  spiTFT.begin(TFT_CLK, TFT_MISO, TFT_MOSI, TFT_CS);
  tft.begin(40000000);
  tft.setRotation(1);
  tft.fillScreen(COL_BG);
  tft.fillRect(0, 0, SCREEN_W, 52, COL_HEADER);
  tft.setTextColor(ILI9341_BLACK); tft.setTextSize(2);
  tft.setCursor(8, 16); tft.print("Demarrage...");
  tft.drawFastHLine(0, 52, SCREEN_W, COL_ORANGE);
  addLog("[SYS] Ecran OK");

  SPI.begin(RFID_SCK, RFID_MISO, RFID_MOSI, RFID_SS);
  rfid.PCD_Init(RFID_SS, RFID_RST);
  delay(50);
  addLog("[SYS] RFID initialise");

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid);
  addLog("[WiFi] Connexion...");
  int t = 0;
  while (WiFi.status() != WL_CONNECTED && t++ < 20) delay(500);
  if (WiFi.status() == WL_CONNECTED) {
    addLog("[WiFi] Connecte OK");
    addLog("[IP]  " + WiFi.localIP().toString());
  } else {
    addLog("[WiFi] Echec connexion", COL_RED);
  }
  drawHeader();

  if (esp_now_init() != ESP_OK) {
    addLog("[ESP-NOW] Erreur init", COL_RED);
    setLED(LED_ROUGE); return;
  }
  esp_now_register_send_cb(onDataSent);
  esp_now_peer_info_t peerInfo = {};
  memcpy(peerInfo.peer_addr, macESP01, 6);
  peerInfo.channel = 0;
  peerInfo.encrypt = false;
  if (esp_now_add_peer(&peerInfo) != ESP_OK) {
    addLog("[ESP-NOW] Erreur peer", COL_RED);
    setLED(LED_ROUGE); return;
  }
  addLog("[ESP-NOW] Pret");

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *r) {
    r->send(200, "text/plain", "Serrure INPHB OK");
  });
  ElegantOTA.begin(&server, ota_username, ota_password);
  server.begin();
  addLog("[OTA] /update dispo", COL_ORANGE);

  setLED(LED_BLEUE);
  addLog("[SYS] Attente badge...");
}

void loop() {
  ElegantOTA.loop();

  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();
  addLog("[RFID] " + uid);

  if (badge_autorise(rfid.uid.uidByte, rfid.uid.size)) {
    addLog("[AUTH] Acces autorise", COL_GREEN);
    showBadgeFlash(true);
    setLED(LED_ORANGE);
    strcpy(msg.commande, "OPEN");
    esp_now_send(macESP01, (uint8_t *)&msg, sizeof(msg));
    delay(500);
    setLED(LED_BLEUE);
    sendLogToSupabase(uid, "accorde");
  } else {
    addLog("[AUTH] Acces refuse", COL_RED);
    showBadgeFlash(false);
    setLED(LED_ROUGE);
    delay(2000);
    setLED(LED_BLEUE);
    sendLogToSupabase(uid, "refuse");
  }

  addLog("[SYS] Attente badge...");
  rfid.PICC_HaltA();
  rfid.PCD_StopCrypto1();
  delay(1000);
}

Program 2 — Door Controller (ESP-01S Relay Module)

Overview

The ESP-01S Relay Module acts as a wireless slave in the system. It connects to the same Wi-Fi network as the ESP32 and listens for ESP-NOW messages. When it receives an "OPEN" command, it activates the relay for 5 seconds, unlocking the electric door lock, then deactivates it automatically.

It also exposes an OTA update endpoint so its firmware can be updated wirelessly without physical access to the module.

Libraries Used

Library Role
ESP8266WiFi.h Wi-Fi connection management for ESP8266
ESPAsyncTCP.h Asynchronous TCP for ESP8266
ESPAsyncWebServer.h Asynchronous HTTP web server
ElegantOTA.h Web-based OTA firmware update interface
espnow.h ESP-NOW protocol for ESP8266

Pin Configuration

Signal GPIO
Relay control GPIO0

Note: On the ESP-01S module, GPIO0 is used to control the onboard relay. RELAY_ON = HIGH activates the relay; RELAY_OFF = LOW deactivates it. Verify the relay logic of your specific module — some relay boards are active LOW.

ESP-NOW Reception

The ESP-01S is configured as an ESP-NOW slave. It registers a receive callback onDataRecv() that is triggered every time a message arrives:

void onDataRecv(uint8_t *mac, uint8_t *data, uint8_t len) {
  memcpy(&messageRecu, data, sizeof(messageRecu));
  if (strcmp(messageRecu.commande, "OPEN") == 0) {
    digitalWrite(RELAY_PIN, RELAY_ON);
    delay(5000);        // hold open for 5 seconds
    digitalWrite(RELAY_PIN, RELAY_OFF);
  }
}

The message structure must be identical on both sides for ESP-NOW to deserialize the payload correctly:

typedef struct {
  char commande[10];
} Message;

Setup Sequence

1. Initialize relay pin → OFF (door locked at startup)
2. Connect to Wi-Fi (STA mode)
3. Print IP and MAC address to Serial monitor
4. Initialize ESP-NOW → set role as SLAVE
5. Register receive callback (onDataRecv)
6. Start OTA web server

Main Loop Logic

Loop:
└── ElegantOTA.loop()    ← handles OTA requests (non-blocking)
    (no other logic — everything is event-driven via ESP-NOW callback)

The loop is intentionally empty except for OTA handling. All door-control logic is triggered asynchronously by the ESP-NOW receive callback.

Complete Code — Program 2

door_controller_esp01s.ino
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ElegantOTA.h>
#include <espnow.h>

const char* ssid         = "WiFi INPHB NEWS";
const char* ota_username = "mpass";
const char* ota_password = "25vfvft56]@224044";

#define RELAY_PIN 0       // GPIO0 on ESP-01S
#define RELAY_ON  HIGH    // HIGH = relay activated
#define RELAY_OFF LOW

AsyncWebServer server(80);

// Message structure — must be identical to Program 1
typedef struct {
  char commande[10];
} Message;

Message messageRecu;

// ESP-NOW receive callback — triggered on incoming message
void onDataRecv(uint8_t *mac, uint8_t *data, uint8_t len) {
  memcpy(&messageRecu, data, sizeof(messageRecu));
  Serial.printf("Message from: %02X:%02X:%02X:%02X:%02X:%02X\n",
                mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
  Serial.printf("Command: %s\n", messageRecu.commande);

  if (strcmp(messageRecu.commande, "OPEN") == 0) {
    Serial.println("Unlocking door...");
    digitalWrite(RELAY_PIN, RELAY_ON);
    delay(5000);          // hold relay open for 5 seconds
    digitalWrite(RELAY_PIN, RELAY_OFF);
    Serial.println("Door locked.");
  }
}

void setup() {
  Serial.begin(115200);

  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, RELAY_OFF);  // relay off at startup

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid);
  Serial.print("Connecting to WiFi");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nConnected. IP: " + WiFi.localIP().toString());
  Serial.println("MAC: " + WiFi.macAddress());  // copy this into Program 1

  if (esp_now_init() != 0) {
    Serial.println("ESP-NOW init error");
    return;
  }
  esp_now_set_self_role(ESP_NOW_ROLE_SLAVE);
  esp_now_register_recv_cb(onDataRecv);
  Serial.println("ESP-NOW ready.");

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    request->send(200, "text/plain", "Door controller ready.");
  });
  ElegantOTA.begin(&server, ota_username, ota_password);
  server.begin();
  Serial.println("OTA ready at http://" + WiFi.localIP().toString() + "/update");
}

void loop() {
  ElegantOTA.loop();
  // Loop is intentionally minimal — all logic is event-driven
}

10. Assembly and Final Testing

Once all modules were validated individually, I assembled the full system.


Files