Costruisci un Antifurto WiFi con ESP32
Sicurezza IoT: Realizzare un Sistema di Allarme con ESP32 e Protocollo TCP/IP
La sicurezza domestica è una priorità per chiunque, specialmente per monitorare zone vulnerabili come seminterrati, garage o ingressi secondari. In questo articolo, approfondiremo come trasformare due moduli ESP32 in un sistema di anti-intrusione intelligente basato su un’architettura Client-Server.
Non si tratta di un semplice interruttore Wi-Fi, ma di un sistema che sfrutta il protocollo di comunicazione TCP/IP per garantire affidabilità e scalabilità.
Il Cuore del Sistema: Perché l’ESP32?
L’ESP32 non è solo un microcontrollore economico (costa meno di 10€ su Amazon e meno di 5€ su Aliexpress), ma è un dispositivo molto versatile dove è possibile realizzare molti progetti come domotica e IoT.
Architettura Dual-Core e FreeRTOS
A differenza dei classici Arduino con microcontrollore Atmega 328P, l’ESP32 vanta un sistema dual-core a 240 MHz. Il vantaggio fondamentale risiede nella gestione dei processi tramite il sistema operativo real-time FreeRTOS, integrato di default.
- Core 0: Gestisce lo stack di rete e le funzioni Wi-Fi/Bluetooth tramite la libreria LwIP (Lightweight IP).
- Core 1: Esegue esclusivamente lo sketch dell’utente, garantendo che le logiche di allarme non vengano rallentate dalle operazioni di rete.
Analisi Tecnica della Comunicazione: Socket e Handshake
Per questo progetto è stato scelto il protocollo TCP/IP invece dell’UDP, poiché il TCP garantisce la consegna dei pacchetti.
Il processo di connessione (Handshake)
Prima che il sensore possa inviare un allarme, avviene una sincronizzazione in tre fasi tra Client e Server:
- SYN (Synchronize): Il client richiede la connessione.
- SYN-ACK: Il server, se ha risorse libere, risponde confermando la disponibilità.
- ACK (Acknowledge): Il client conferma e stabilisce il canale di comunicazione.

Le funzioni principali del codice

Descrizione del codice
Client:
#include <WiFi.h>
// ── Configurazione ────────────────────────────────────────────────────────────
const char* SSID = "SSID";
const char* PASSWORD = "PASSWORD";
const char* SERVER_IP = "192.168.1.100";
const uint16_t SERVER_PORT = 80;
const uint32_t BAUD = 115200;
const uint16_t TIMEOUT = 3000; // ms
const uint16_t RETRY_DELAY = 5000; // ms tra un tentativo e l'altro
const uint16_t PIR_WARMUP = 30000; // ms di calibrazione HC-SR501
int i = 0;
// ── IP Statico ────────────────────────────────────────────────────────────────
IPAddress LOCAL_IP(192, 168, 1, 101);
IPAddress GATEWAY (192, 168, 1, 1);
IPAddress SUBNET (255, 255, 255, 0);
// ── Pin HC-SR501 ──────────────────────────────────────────────────────────────
const uint8_t PIR_PIN = 4; // GPIO collegato al pin OUT dell'HC-SR501
// ── Prototipi ─────────────────────────────────────────────────────────────────
void connectWiFi();
bool sendMessage(const String& msg);
String readPirState();
// ── Setup ─────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(BAUD);
pinMode(PIR_PIN, INPUT); // HC-SR501 ha uscita driven, no pull-up necessario
// Attesa calibrazione sensore PIR
Serial.println("[PIR] Calibrazione HC-SR501 in corso (30s)...");
delay(PIR_WARMUP);
Serial.println("[PIR] Sensore pronto!");
connectWiFi();
}
// ── Loop ──────────────────────────────────────────────────────────────────────
void loop() {
// Riconnessione automatica se il WiFi cade
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WiFi] Connessione persa. Riconnessione...");
connectWiFi();
}
// Legge lo stato del PIR e invia il messaggio al server
String msg = readPirState();
Serial.println("[CLIENT] Invio messaggio: " + msg);
if (sendMessage(msg)) {
Serial.println("[CLIENT] Messaggio inviato con successo.");
} else {
Serial.println("[CLIENT] Invio fallito. Ritento tra " + String(RETRY_DELAY / 1000) + "s...");
}
delay(RETRY_DELAY);
}
// ── Funzioni ──────────────────────────────────────────────────────────────────
/**
* Legge il pin OUT dell'HC-SR501.
* HIGH = movimento rilevato → "alarm"
* LOW = nessun movimento → "ok"
*/
String readPirState() {
return (digitalRead(PIR_PIN) == HIGH) ? "ok" : "alarm";
//if (i == 0) {
// i = 1;
//return "alarm";}
// else{
//i = 0;
//return "ok";
// }
}
/**
* Apre una connessione TCP al server, invia il messaggio
* e aspetta l'ACK. Restituisce true se tutto va a buon fine.
*/
bool sendMessage(const String& msg) {
WiFiClient client;
client.setTimeout(TIMEOUT);
if (!client.connect(SERVER_IP, SERVER_PORT)) {
Serial.println("[CLIENT] Impossibile connettersi al server.");
return false;
}
client.println(msg);
client.flush();
// Attende l'ACK dal server
uint32_t start = millis();
while (!client.available()) {
if (millis() - start > TIMEOUT) {
Serial.println("[CLIENT] Timeout in attesa di risposta.");
client.stop();
return false;
}
delay(10);
}
String response = client.readStringUntil('\n');
response.trim();
Serial.println("[SERVER] Risposta: " + response);
client.stop();
return true;
}
/**
* Gestisce la connessione WiFi con IP statico e tentativi multipli.
* In caso di fallimento ripetuto, riavvia l'ESP32.
*/
void connectWiFi() {
Serial.printf("[WiFi] Connessione a %s...\n", SSID);
WiFi.mode(WIFI_STA);
if (!WiFi.config(LOCAL_IP, GATEWAY, SUBNET)) {
Serial.println("[WiFi] Configurazione IP statico fallita!");
}
WiFi.begin(SSID, PASSWORD);
uint8_t attempts = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (++attempts >= 20) {
Serial.println("\n[WiFi] Connessione fallita. Riavvio...");
ESP.restart();
}
}
Serial.printf("\n[WiFi] Connesso! IP: %s\n", WiFi.localIP().toString().c_str());
}
Server
#include <WiFi.h>
// ── Configurazione ────────────────────────────────────────────────────────────
const char* SSID = "SSID";
const char* PASSWORD = "PASSWORD";
const uint16_t PORT = 80;
const uint32_t BAUD = 115200;
const uint16_t TIMEOUT = 3000; // ms
// ── IP Statico ────────────────────────────────────────────────────────────────
IPAddress LOCAL_IP(192, 168, 1, 100); // IP fisso desiderato
IPAddress GATEWAY(192, 168, 1, 1); // IP del tuo router
IPAddress SUBNET(255, 255, 255, 0); // Subnet mask
WiFiServer server(PORT);
const uint8_t BUZZER = 2; // cambia con il pin che vuoi usare
// ── Prototipi ─────────────────────────────────────────────────────────────────
void connectWiFi();
void handleClient(WiFiClient& client);
// ── Setup ─────────────────────────────────────────────────────────────────────
void setup() {
Serial.begin(BAUD);
connectWiFi();
server.begin();
Serial.println("[SERVER] In ascolto sulla porta " + String(PORT));
pinMode(BUZZER, OUTPUT);
digitalWrite(BUZZER, LOW); // stato iniziale spento
}
// ── Loop ──────────────────────────────────────────────────────────────────────
void loop() {
if (WiFi.status() != WL_CONNECTED) {
Serial.println("[WiFi] Connessione persa. Riconnessione...");
connectWiFi();
}
WiFiClient client = server.available();
if (client) {
handleClient(client);
}
}
// ── Funzioni ──────────────────────────────────────────────────────────────────
void connectWiFi() {
Serial.printf("[WiFi] Connessione a %s...\n", SSID);
WiFi.mode(WIFI_STA);
// Configura IP statico PRIMA di WiFi.begin()
if (!WiFi.config(LOCAL_IP, GATEWAY, SUBNET)) {
Serial.println("[WiFi] Configurazione IP statico fallita!");
}
WiFi.begin(SSID, PASSWORD);
uint8_t attempts = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
if (++attempts >= 20) {
Serial.println("\n[WiFi] Connessione fallita. Riavvio...");
ESP.restart();
}
}
Serial.printf("\n[WiFi] Connesso! IP: %s\n", WiFi.localIP().toString().c_str());
}
void handleClient(WiFiClient& client) {
Serial.printf("[CLIENT] Connesso da %s\n", client.remoteIP().toString().c_str());
client.setTimeout(TIMEOUT);
uint32_t lastActivity = millis();
while (client.connected()) {
if (millis() - lastActivity > TIMEOUT) {
Serial.println("[CLIENT] Timeout. Chiusura connessione.");
break;
}
if (client.available()) {
String msg = client.readStringUntil('\n');
msg.trim();
if (msg.length() == 0) continue;
Serial.println("[CLIENT] Ricevuto: " + msg);
lastActivity = millis();
client.println("ACK: " + msg);
if (msg == "alarm") {
//attiva l'allarme
digitalWrite(BUZZER, HIGH);
Serial.println("[ACTION] Allarme attivato!");
} else if (msg == "ok") {
// disattiva l'allarme
digitalWrite(BUZZER, LOW); // spegne l'uscita
Serial.println("[ACTION] Allarme disattivato.");
}
client.flush();
}
yield();
}
client.stop();
Serial.println("[CLIENT] Disconnesso.");
}
Descrizione codie:
Client:
void setup()
Cosa fa:
- Inizializza la comunicazione seriale
- Configura il pin del sensore PIR
- Attende la calibrazione del sensore
- Stabilisce la connessione WiFi
Perché è importante:
Questa funzione prepara tutto il sistema prima di entrare nel ciclo operativo.
La pausa di 30 secondi (PIR_WARMUP) è fondamentale per stabilizzare il sensore HC-SR501 ed evitare falsi allarmi iniziali.
oid loop()
Cosa fa:
- Controlla che il WiFi sia ancora connesso
- Legge lo stato del sensore PIR
- Invia il messaggio al server
- Attende prima di ripetere il ciclo
Perché è importante:
È il cuore del programma.
Implementa un ciclo continuo di:
lettura → trasmissione → attesa → ripetizione
Inoltre include un meccanismo di riconnessione automatica nel caso il WiFi cada.
tring readPirState()
Cosa fa:
- Legge il livello logico del pin collegato al PIR
- Traduce il segnale elettrico in un messaggio applicativo
Restituisce:
"alarm"→ movimento rilevato"ok"→ nessun movimento
Perché è importante:
Qui avviene la separazione tra:
- Livello hardware (GPIO)
- Livello logico/applicativo (stringa da inviare via TCP)
È il punto in cui un evento fisico diventa informazione di rete.
sendMessage(const String& msg)
bool sendMessage(const String& msg)
Cosa fa:
- Crea un client TCP
- Si connette al server
- Invia il messaggio
- Attende una risposta (ACK)
- Gestisce eventuale timeout
- Chiude la connessione
Perché è importante:
È la funzione che implementa la comunicazione TCP.
Include:
- Timeout di sicurezza
- Controllo errore
- Attesa risposta dal server
Se qualcosa va storto, restituisce false, permettendo al sistema di riprovare.
Server
server.begin()
Cosa fa:
Avvia il server TCP.
In pratica:
- Crea una socket
- Associa una porta (es. 80)
- Mette il server in ascolto
È il punto di partenza per accettare connessioni in ingresso.
2️⃣ server.available()
Cosa fa:
Controlla se un client ha richiesto la connessione.
In pratica:
- Verifica se c’è una connessione in attesa
- Se presente, restituisce un oggetto
WiFiClient - Se non c’è nulla, restituisce un client “vuoto”
Serve per accettare nuove connessioni.
3️⃣ client.readStringUntil()
Cosa fa:
Legge i dati ricevuti fino a un carattere specifico (es. \n).
In pratica:
- Legge dal buffer di ricezione
- Converte i byte in una
String - Si ferma quando trova il delimitatore
È una funzione comoda per leggere messaggi testuali.
4️⃣ client.println()
Cosa fa:
Invia una stringa al client/server seguita da newline.
In pratica:
- Scrive i dati nel buffer di trasmissione
- Aggiunge
\r\n - Avvia la trasmissione TCP
È una funzione di invio dati ad alto livello.
socket()
Cosa fa:
Crea un endpoint di comunicazione.
In pratica:
- Alloca una struttura di comunicazione
- Restituisce un file descriptor
È il primo passo per qualsiasi comunicazione TCP.
bind()
Cosa fa:
Associa la socket a un indirizzo IP e a una porta.
In pratica:
- “Prenota” una porta (es. 80)
- Dice al sistema dove ricevere i pacchetti
listen()
Cosa fa:
Mette la socket in modalità ascolto.
In pratica:
- Abilita la coda delle connessioni in ingresso
- Prepara il server a ricevere richieste
accept()
Cosa fa:
Accetta una connessione in arrivo.
In pratica:
- Estrae una connessione dalla coda
- Crea una nuova socket dedicata al client
Da questo momento client e server possono comunicare.
recv()
Cosa fa:
Riceve dati dalla socket.
In pratica:
- Legge dal buffer TCP
- Copia i byte ricevuti in memoria
È la versione low-level di readStringUntil().
send()
Cosa fa:
Invia dati sulla socket.
In pratica:
- Copia i dati nel buffer di trasmissione
- Lo stack TCP li impacchetta e li invia
È la versione low-level di println().
In Sintesi
Quando scrivi:
server.begin();
In realtà stai usando:
socket() + bind() + listen()
L’API Arduino ti nasconde tutta la complessità delle socket POSIX.
Metafora della “Comunicazione Telefonica”
Quando Per comprendere il codice, possiamo immaginare la creazione del server in tre step:
- Socket: Equivale all’acquisto del telefono (allocazione di memoria RAM per i messaggi).
- Bind: È l’inserimento della SIM (associazione di una porta e di un indirizzo IP statico, come il 192.168.1.100, essenziale affinché il client trovi sempre il server dopo un blackout).
- Listen/Accept: Il server resta in attesa di uno “squillo” dal client per accettare la chiamata.
Logica del Software e Ottimizzazione
Il sistema è progettato per essere “resiliente”. Ecco le chicche tecniche implementate nello sketch:
- Modalità Solo Station (STA): Il modulo è configurato per connettersi solo al Wi-Fi esistente senza attivare la funzione di Access Point, risparmiando energia e risorse.
- Heartbeat (“OK”): Anche se non c’è un’intrusione, il client invia periodicamente un messaggio “OK”. Questo serve al server per capire che il client è ancora alimentato e connesso.
- Timeout di Sicurezza: Se il client smette di comunicare per più di 3 secondi, il server chiude la connessione per liberare la memoria socket per altri potenziali dispositivi.
- Forzatura del Buffer: Poiché il protocollo TCP tende ad aspettare che il buffer sia pieno (circa 1500 byte) prima di spedire, nel codice viene usata una funzione per forzare l’invio immediato della stringa “ALARM”, riducendo a zero la latenza della segnalazione.
Implementazione Pratica
Nel prototipo mostrato, il Client monitora un sensore (simulato con un interruttore sul pin D4), mentre il Server attua l’allarme tramite un LED blu (che sostituisce un buzzer acustico per comodità di test).
- Quando il sensore rileva movimento (segnale a 3.3V), il client invia la stringa “ALARM”.
- Il server riceve la stringa e porta il pin dell’attuatore su HIGH, accendendo l’allarme.


Scalabilità: Verso una Smart Home
Sebbene l’esempio utilizzi un solo client, l’architettura permette di collegare più client distribuiti in vari punti della casa, tutti comunicanti con lo stesso server centrale.







