Sicurezza domestica: controllo accessi con ESP8266, RFID e Telegram

Introduzione

Nell’era digitale in cui viviamo, la sicurezza domestica realizzata tramite un efficace controllo accessi è diventata una priorità per molti. Con l’avanzamento delle tecnologie, è possibile creare soluzioni di sicurezza intelligenti e personalizzate che non solo migliorano la sicurezza, ma anche offrono comodità e flessibilità. In questo articolo, esploreremo un progetto innovativo che combina ESP8266, RFID, Telegram e API REST per creare un sistema di controllo degli accessi domestico.

L’ESP8266, un microcontroller Wi-Fi, è il cuore di questo progetto, permettendo una comunicazione semplice e diretta con Internet. Il modulo RFID, un sistema di identificazione senza contatto, viene utilizzato per l’identificazione degli utenti, garantendo un controllo degli accessi sicuro e conveniente. Infine, il bot Telegram viene integrato per inviare notifiche in tempo reale, mantenendo gli utenti informati sugli accessi e fornendo un modo facile per gestire le autorizzazioni.

Questo sistema non è solo un modo per migliorare la sicurezza domestica, ma anche un esempio di come l’Internet delle Cose (IoT) può essere utilizzato per creare soluzioni personalizzate che rispondono alle esigenze specifiche di sicurezza. Attraverso l’uso di API REST, il sistema permette una gestione semplice e intuitiva delle autorizzazioni, offrendo la possibilità di aggiungere, modificare o eliminare utenti con facilità (operazioni eseguibili anche tramite il bot Telegram).

Nel corso di questo articolo, esploreremo come è stato implementato il progetto, come funziona il sistema di controllo degli accessi, e come le API REST e il bot Telegram sono stati integrati per offrire una soluzione completa. Questo progetto rappresenta un esempio di come l’innovazione può essere utilizzata per risolvere problemi comuni in modo creativo e efficace.

Anche in questo progetto useremo l’IDE PlatformIO per la stesura del codice.

Descrizione del funzionamento

Il nostro progetto di sicurezza domestica si basa su un sistema automatizzato che integra tecnologie avanzate per garantire l’accesso sicuro e controllato. Ecco come funziona:

  1. Inizializzazione del Sistema: Il sistema contiene sulla SD card un file CSV di nome auth.txt (che dovremo inizialmente creare noi) che contiene, per ogni riga, l’UUID di un utente e i suoi privilegi di accesso (“g” per granted e “d” per denied) separati da una virgola. All’inizio l’ESP8266 legge questo file e lo carica su un dizionario (di cui parleremo in uno dei successivi paragrafi).
  1. Identificazione dell’Utente: Quando un badge viene avvicinato al modulo RFID, il sistema legge l’UUID (Universally Unique Identifier) del badge. Questo UUID è un identificatore univoco per ogni badge, utilizzato per distinguere gli utenti.
  1. Controllo delle Autorizzazioni: Dopo aver letto l’UUID, il sistema controlla il dizionario per determinare se l’utente ha il permesso di accedere.
  1. Decisione di Accesso: In base al permesso associato all’UUID, il sistema decide se far scattare il relè, consentendo o negando l’accesso. Se l’UUID è associato a un permesso concesso (“g”), il relè si attiva, consentendo l’accesso. Se il permesso è negato (“d”), l’accesso viene negato. Inoltre, nel primo caso si accende un LED verde mentre nel secondo caso si accende un LED rosso. Il LED rosso rimane acceso anche quando il sistema è nello stato “inattivo” cioè sta aspettando che venga avvicinato un tag RFID al lettore.
  1. Registrazione degli Accessi: Ogni tentativo di accesso viene registrato su un file CSV nella SD card. Questo fornisce un registro temporale degli accessi, utile per il monitoraggio e la sicurezza. Per ogni riga viene registrato l’UUID, l’ora (derivata dal modulo RTC) e se l’ingresso è stato concesso (“g”) o negato (“d”).
  2. Struttura Organizzata delle Cartelle e dei File: la scheda SD organizza le informazioni in modo intuitivo, suddividendole in directory corrispondenti all’anno e al mese in corso, con file giornalieri che contengono i dati descritti al punto precedente. Quindi, per fare un esempio, le rilevazioni degli accessi di un determinato giorno (per esempio il 26 gennaio 2024) si troveranno nel file con path templog/24/01/26
  3. Notifiche Telegram: Contemporaneamente all’accesso, il sistema invia un messaggio di avviso al bot Telegram. Questo permette agli utenti di ricevere notifiche in tempo reale sugli accessi, aumentando la consapevolezza e la sicurezza.
  4. Gestione delle Autorizzazioni tramite API REST e tramite bot Telegram: Il sistema è ulteriormente integrato con un server che espone API REST per la gestione delle autorizzazioni. Queste API permettono di ottenere la data e l’ora corrente, leggere il file delle autorizzazioni, aggiungere un nuovo utente con i suoi privilegi, modificare i privilegi di un utente esistente o eliminare un utente. Tramite il bot Telegram si potrà aggiungere un nuovo utente (UUID) con i suoi privilegi o modificare i privilegi di un utente già esistente, eliminare un utente, leggere i privilegi di accesso di un dato utente. In entrambi i casi (API REST o bot Telegram) le modifiche alle autorizzazioni saranno permanenti in quanto verranno salvate sul file auth.txt.

Cosa è la tecnologia RFID

Il Radio-Frequency Identification (RFID) è un sistema di identificazione automatica che utilizza campi elettromagnetici per trasferire dati tra un lettore e una tag RFID. Questa tecnologia è basata sulla comunicazione senza contatto e consente la memorizzazione e il recupero di informazioni da oggetti, animali o persone dotati di tag RFID. Ecco alcuni punti chiave relativi al funzionamento e all’utilizzo dell’RFID:

  1. Principio di funzionamento:
    • Un sistema RFID è composto da un lettore (o interrogatore) e una o più tag RFID.
    • La tag contiene un chip che memorizza un codice univoco o altre informazioni.
    • Quando la tag si trova nel campo elettromagnetico del lettore, riceve energia e trasmette i dati memorizzati al lettore.
  2. Frequenze di funzionamento:
    • Gli RFID operano a diverse frequenze, suddivise principalmente in bassa frequenza (LF), alta frequenza (HF), ultra alta frequenza (UHF), e frequenza molto alta (VHF).
    • Le diverse frequenze influenzano la distanza di lettura e la capacità di penetrare attraverso materiali.
  3. Tipi di tag RFID:
    • Le tag possono essere attive (con batteria) o passive (senza batteria).
    • Le tag passive sono alimentate dal campo del lettore e hanno una distanza di lettura più breve rispetto alle tag attive.
  4. Applicazioni comuni:
    • Controllo degli accessi: badge RFID per aperture automatiche o autenticazioni di sicurezza.
    • Logistica e tracciabilità: monitoraggio di merci in transito attraverso magazzini e catene di distribuzione.
    • Pagamenti contactless: sistemi di pagamento senza contatto come le carte contactless.
  5. Sicurezza e privacy:
    • L’RFID può presentare sfide legate alla sicurezza e alla privacy, con la possibilità di intercettare e clonare i dati.
    • Sono state sviluppate varie tecniche di crittografia per proteggere le informazioni scambiate tra i lettori e le tag.
  6. Implementazione con ESP8266:
    • L’ESP8266 può essere utilizzato come lettore RFID, interfacciandosi con un modulo RFID compatibile.
    • Le informazioni lette dalla tag RFID possono essere utilizzate per attivare specifiche azioni o accedere a risorse.

L’utilizzo di RFID offre un’efficace e conveniente soluzione per molteplici applicazioni, consentendo l’automazione di processi e migliorando l’efficienza operativa.

Il kit RFID impiegato in questo progetto

Il kit, in questo caso distribuito dall’azienda ICQUANZX (ma esiste anche con marchi diversi), si compone di un lettore RFID, un badge, un portachiavi (sempre RFID), un connettore da saldare sul lettore e alcuni fili. Se non hai dimestichezza col mondo dei saldatori e vuoi provare a saldare il connettore in autonomia, ti consiglio di dare uno sguardo all’articolo Un altro tutorial su come saldare.

Sia il badge che il portachiavi contengono un codice che può essere letto dal lettore RFID. Nel mio caso il badge ha codice esadecimale 73051E99 mentre il portachiavi ha codice esadecimale A305CD0B.

Ai fini del progetto ho impostato il file delle autorizzazioni in modo che il codice A305CD0B possa far scattare il relè mentre al codice 73051E99 tale operazione sia inibita. Tanto per essere più chiari, ho momentaneamente inserito la SD card nello slot del computer, in essa ho creato un file di nome auth.txt e dentro ho scritto le due righe:

A305CD0B,g
73051E99,d

Ho poi rimosso in sicurezza la SD card dal computer e l’ho reinserita nel modulo SD card collegato all’ESP8266.

Ma come si fa a scoprire che codici UUID contengono i nostri tag? Inizialmente è sufficiente tenere il file auth.txt vuoto, poi avvicinare un tag al lettore e leggere il suo UUID stampato sul Serial Monitor. Ripetere l’operazione per tutti i tag in nostro possesso. A questo punto si può estrarre la SD card dal modulo, metterla nel computer e compilarla con un semplice editor di testo. Alternativamente, senza estrarre la SD card, una volta ottenuti gli UUID si possono utilizzare le operazioni di inserimento degli utenti con i loro privilegi fornite dal bot Telegram o dalle API REST.

Il modulo di lettura RFID può intefacciarsi all’ESP8266 tramite comunicazione sia SPI che I2C di cui parleremo nel paragrafo successivo. Il modulo RFID utilizzato ha degli switch che consentono di selezionare l’interfaccia voluta fra SPI, I2C e UART. In questo progetto comunicherà con l’interfaccia I2C.

Nonostante il kit sia corredato di un connettore maschio, io ho preferito saldare sulla sua porta I2C un connettore femmina in quanto lo ritengo più comodo. Ovviamente tu potrai mettere il connettore che preferisci.

Avrei potuto anche usare il bus SPI e avere collegati, quindi, sullo stesso bus SPI sia il lettore SD card che il lettore RFID. Il problema è che purtroppo non sono compatibili, nel senso che il modello utilizzato di SD card su bus SPI non consente (per un suo problema hardware) ad altri dispositivi SPI di funzionare correttamente. Quindi deve stare da solo su tale bus. La soluzione è stata quella di trovare un lettore RFID che avesse anche interfaccia I2C in modo da collegarli su bus differenti. Sul bus I2C, oltre al lettore RFID, è collegato anche il modulo RTC e i due convivono tranquillamente avendo indirizzi diversi.

Nella foto successiva potrai vedere il kit completo:

Kit RFID PN532 completo
Kit RFID PN532 completo
Particolare del connettore femmina saldato sul bus I2C
Particolare del connettore femmina saldato sul bus I2C
Impostazione per l'uso del bus I2C
Impostazione per l’uso del bus I2C

Come puoi notare dalla foto precedente, per poter impostare la comunicazione sul bus I2C devi spostare lo switch 1 verso destra e lo switch 2 verso sinistra. Potresti trovare una piccola pellicola arancione semitrasparente a protezione dello switch. Rimuovila prima di fare l’operazione di settaggio del tipo di bus.

Interfaccia SPI

Il modulo lettore RFID può usare l’interfaccia SPI, che è un protocollo seriale di comunicazione a 4 fili comunemente utilizzato in progetti embedded. Questi quattro fili sono:

  1. MISO (Master In Slave Out): questo è il pin attraverso il quale il modulo riceve dati dal dispositivo master, che di solito è l’Arduino o un altro microcontrollore.
  2. MOSI (Master Out Slave In): questo è il pin attraverso il quale il modulo invia dati al dispositivo master.
  3. SCK (Serial Clock): questo è il pin del clock che sincronizza la trasmissione dei dati tra il modulo e il dispositivo master.
  4. CS (Chip Select): questo pin viene utilizzato per selezionare il modulo RFID e inizializzare le operazioni di invio/ricezione dei dati.

Interfaccia I2C

In alternativa il modulo lettore RFID può usare l’interfaccia I2C. Il protocollo I2C, noto anche come Two-Wire Interface (TWI), è uno standard di comunicazione seriale che consente la trasmissione di dati tra dispositivi elettronici. Questo protocollo è ampiamente utilizzato in molte applicazioni, inclusi i microcontroller, per facilitare la comunicazione tra diversi componenti di un sistema. Il protocollo I2C è caratterizzato da alcune caratteristiche chiave che lo rendono ideale per applicazioni di basso consumo energetico e con limitate risorse hardware.

  • Comunicazione bidirezionale: I2C supporta la comunicazione bidirezionale, permettendo a un dispositivo di inviare dati a un altro dispositivo e viceversa. Questa caratteristica è particolarmente utile in sistemi che richiedono un’interazione continua tra componenti.
  • Uso di due linee: Come suggerisce il nome, I2C utilizza solo due linee per la comunicazione: una per il clock (SCL) e una per i dati (SDA). Questo riduce significativamente il numero di pin necessari per la comunicazione, rendendo il protocollo ideale per dispositivi con limitate risorse di pin.
  • Gestione dell’indirizzo: Ogni dispositivo I2C ha un indirizzo unico, permettendo a più dispositivi di comunicare sulla stessa linea di clock e dati. Questo sistema di indirizzamento consente una comunicazione efficiente e organizzata tra i dispositivi. È da rimarcare il fatto che su tale bus non possono coesistere dispositivi con lo stesso indirizzo.
  • Comunicazione con velocità standard e a bassa velocità: I2C supporta velocità di comunicazione standard fino a 100 kHz e a bassa velocità fino a 400 kHz. Queste velocità sono sufficienti per la maggior parte delle applicazioni, rendendo il protocollo adatto per dispositivi che non richiedono velocità di comunicazione elevate.
  • Compatibilità e facilità d’uso: Il protocollo I2C è ampiamente supportato da molte piattaforme e dispositivi, rendendolo una scelta popolare per la comunicazione tra componenti. La sua semplicità e la presenza di molte librerie e documentazione lo rendono facile da implementare e da utilizzare.

Il protocollo I2C è un esempio perfetto di come la progettazione di protocolli di comunicazione possa essere semplice e potente, offrendo una soluzione efficiente e versatile per la comunicazione tra dispositivi in un’ampia gamma di applicazioni.

Cosa è un dizionario

Abbiamo già detto che nella fase di setup l’ESP8266 legge dalla SD card il file delle autorizzazioni auth.txt e lo carica in un dizionario in modo da avere queste informazioni già in memoria senza dover ogni volta rileggere il file.

Tale file, essendo un file CSV, avrà un aspetto del genere:

A305CD0B,g
73051E99,d

Si può osservare che all’utente con UUID A305CD0B è stato associato il privilegio “g” (granted) quindi potrà accedere mentre l’accesso è negato all’utente con UUID 73051E99 (“d” per denied).

  1. Definizione di dizionario:
    • In programmazione, un dizionario è una struttura dati che consente di memorizzare coppie chiave-valore.
    • Ogni elemento del dizionario è costituito da una chiave unica associata a un valore.
  2. Struttura e accesso:
    • I dizionari sono strutturati in modo da consentire un accesso rapido ai valori tramite le chiavi.
    • L’accesso ai valori avviene specificando la chiave associata, consentendo un recupero efficiente delle informazioni.
  3. Chiavi e Valori:
    • Le chiavi in un dizionario sono di solito stringhe, numeri o altri tipi di dati immutabili.
    • I valori possono essere di qualsiasi tipo, inclusi numeri, stringhe, liste o addirittura altri dizionari.
  4. Utilità e applicazioni:
    • I dizionari sono ampiamente utilizzati per gestire dati strutturati e associare informazioni in modo flessibile.
    • Sono particolarmente utili quando si deve accedere ai dati tramite identificatori univoci anziché posizioni fisse.
  5. Operazioni comuni:
    • Aggiunta di nuove coppie chiave-valore: mioDizionario[chiave] = valore.
    • Rimozione di una coppia: delete mioDizionario[chiave].
    • Recupero del valore associato a una chiave: valore = mioDizionario[chiave].
  6. Iterazione:
    • È possibile iterare su tutte le chiavi, i valori o le coppie chiave-valore del dizionario.
    • Ciò consente di eseguire operazioni su tutti gli elementi del dizionario in modo efficiente.
    • È possibile estrarre la lista di tutte le chiavi o di tutti i valori presenti nel dizionario.
  7. Implementazione nell’ESP8266:
    • In contesti di programmazione per ESP8266, i dizionari sono spesso utilizzati per gestire configurazioni, associazioni chiave-valore dinamiche o mappare informazioni rilevanti.
  8. Efficienza e complessità:
    • L’efficienza nell’accesso ai dati tramite chiavi rende i dizionari una scelta ideale per situazioni in cui è necessario un recupero veloce delle informazioni.
    • La complessità temporale per le operazioni di ricerca, inserimento e cancellazione è spesso molto bassa rispetto ad altre strutture dati.

L’utilizzo di dizionari aggiunge un livello di flessibilità e organizzazione ai programmi, consentendo una gestione efficiente e dinamica delle informazioni.

Nel nostro caso il dizionario caricato a partire dal file auth.txt avrà un aspetto del genere: {“A305CD0B” : “g”, “73051E99” : “d”}.

Il set di API REST a disposizione

Il dispositivo comunica con l’esterno tramite un set di 7 API REST, 2 di tipo GET e 5 di tipo POST:

  • getDate (GET) restituisce un documento Json contenente la data e l’ora correnti nel modulo RTC (anno, mese, giorno, ore, minuti, secondi). Si utilizza per controllare l’esattezza della data e dell’ora;
  • readAuth (GET) restituisce il contenuto del file auth.txt contenente le autorizzazioni per ciascun utente;
  • fileExists (POST) restituisce vero o falso a seconda che un certo file di log degli accessi sia presente nel file system o meno. I dati per permettergli di costruire il path corretto del file sono anno, mese, giorno inviati tramite un apposito Json del tipo:
                      {
                             "year": "24",
                             "month" : "03",
                             "day" : "02"
                       }
  • fileRead (POST) restituisce il contenuto di un determinato file di log degli accessi. I dati per permettergli di costruire il path corretto del file sono anno, mese, giorno inviati tramite un apposito Json del tipo:
                      {
                             "year": "24",
                             "month" : "03",
                             "day" : "02"
                       }
  • setDate (POST) regola l’orologio con i dati di anno, mese, giorno, ore, minuti e secondi forniti con un Json del tipo:
                      {
                             "year": "24",
                             "month" : "03",
                             "day" : "02",
                             "hour" : "15",
                             "minutes" : "12",
                             "seconds" : "10"
                       }
  • addModUser (POST) aggiunge al file auth.txt un utente con i suoi privilegi o modifica i privilegi di un utente già esistente. Un possibile Json di esempio è il seguente:
                      {
                           "uuid" : "73051E99",
                           "auth" : "d"
                       }
  • delUser (POST) elimina un utente con i suoi privilegi dal file auth.txt. Un possibile Json di esempio è il seguente:
                      {
                             "uuid" : "73051E99"
                       }

Il set di comandi Telegram a disposizione

Il bot Telegram riceve una notifica ogni volta che viene tentato un accesso (cioè ogni volta che un tag RFID viene rilevato dal sensore) che indica l’UUID e il privilegio di accesso ad esso associato. Ma è anche in grado di modificare i permessi contenuti nel file auth.txt tramite i seguenti comandi:

  • addmod:uuid:permission aggiunge un utente con i suoi privilegi (g o d) o modifica i privilegi di un utente già esistente (esempio: /addmod:A305CD0B:d)
  • del:uuid rimuove un utente (esempio: /del:A305CD0B)
  • readuser:uuid restituisce i privilegi dell’utente specificato (esempio /readuser:A305CD0B)

Di che componenti abbiamo bisogno per il controllo accessi RFID?

La lista dei componenti non è particolarmente lunga:

  • una breadboard per connettere la NodeMCU ESP8266 agli altri componenti
  • alcuni fili DuPont (maschio – maschio, maschio – femmina, femmina – femmina)
  • un resistore da 100Ω
  • un resistore da 82Ω
  • un LED verde
  • un LED rosso
  • un modulo RFID PN532
  • un modulo con doppio/singolo relè optoisolato
  • un modulo RTC DS3231
  • un modulo di lettura/scrittura di micro SD card con interfaccia SPI
  • una micro SD card da non più di 32GB formattata in FAT32
  • e, ovviamente, una NodeMCU ESP8266 !

Vediamo ora più in dettaglio questi componenti.

Il modulo doppio relè

  1. Alimentazione:
    • Accetta un’ampia gamma di tensioni di alimentazione, di solito compresa tra 5V e 12V.
    • Il connettore di alimentazione è progettato per essere facilmente collegato a una sorgente di alimentazione esterna, come una batteria o un alimentatore.
  2. Relè:
    • Due relè a bordo, ciascuno con i propri contatti elettrici: comune (COM), normale aperto (NO) e normale chiuso (NC).
    • I contatti del relè sono progettati per gestire carichi di potenza. Tuttavia, le specifiche esatte dipendono dal modello specifico del modulo relè.
  3. Ingressi di Controllo:
    • Due ingressi di controllo (IN1 e IN2) che possono essere collegati a pin digitali di una scheda di sviluppo tipo Arduino.
    • Attivare uno di questi ingressi con un segnale logico alto (o basso, a seconda dei casi) attiverà il relè corrispondente.
  4. Indicatori LED:
    • Indicatori LED incorporati per ogni relè che indicano lo stato di attivazione (spesso con colori come rosso per attivato e spento per disattivato).
  5. Compatibilità Arduino:
    • Progettato per essere facilmente integrato con piattaforme di sviluppo come Arduino, rendendo il controllo dei relè un’operazione semplice e accessibile.
  6. Carichi Pilotabili:
    • In grado di pilotare una varietà di carichi elettrici come lampadine, motori, elettrovalvole, e altri dispositivi che richiedono controllo on/off.
    • Le specifiche esatte del carico dipendono dal modello del relè, ma spesso possono gestire carichi con tensioni alternate fino a 250V e correnti fino a 10A.

Questi moduli relè sono ampiamente utilizzati in progetti di domotica, automazione elettronica e controlli remoti, fornendo un’interfaccia sicura e controllata per dispositivi di potenza.

NOTA: in questo progetto viene utilizzato un modulo doppio relè optoisolato ma useremo solo uno dei due relè. Quindi puoi anche scegliere un modulo a singolo relè optoisolato oppure usare quello doppio magari facendo attivare uno dei due relè da un badge e l’altro relè da un altro badge o dal portachiavi.

Un esempio di modulo con doppio relè optoisolato usato dal controllo accessi RFID
Un esempio di modulo con doppio relè optoisolato usato dal controllo accessi RFID

Il modulo DS3231

L’RTC DS3231 (Real-Time Clock DS3231) è un componente elettronico molto utilizzato per tenere traccia del tempo in applicazioni embedded. La sua interfaccia I2C (Inter-Integrated Circuit) lo rende facile da integrare con microcontrollori, come l’Arduino, e altri dispositivi digitali (tipo ESP8266 e ESP32). Quello utilizzato in questo articolo è prodotto dall’azienda AZDelivery. Vediamo una descrizione accurata del DS3231:

Precisione estrema

Il DS3231 è noto per la sua straordinaria precisione nel tenere traccia del tempo. Ha un errore massimo di appena alcuni secondi all’anno, il che lo rende ideale per applicazioni che richiedono marcature temporali accurate.

Interfaccia I2C

L’RTC DS3231 comunica tramite l’interfaccia I2C (o I-squared-C). L’I2C è un protocollo di comunicazione seriale che consente di collegare più dispositivi su un singolo bus, rendendo il DS3231 ideale per progetti che richiedono una gestione semplice e efficiente del tempo.

Calendario completo

Oltre a tenere traccia dell’orario, il DS3231 gestisce anche un calendario completo, compresi giorni della settimana, mesi e anni, considerando anche gli anni bisestili. Questa funzionalità lo rende utile in applicazioni come sveglie, calendari digitali e orologi in tempo reale.

Memoria EEPROM integrata

Il DS3231 è dotato di una piccola memoria EEPROM (Electrically Erasable Programmable Read-Only Memory) che può essere utilizzata per memorizzare dati aggiuntivi. Questa memoria è non volatile, il che significa che i dati rimangono conservati anche in assenza di alimentazione elettrica.

Allarme configurabile

Puoi configurare due allarmi separati sul DS3231, consentendo al dispositivo di generare un segnale di interruzione o un segnale di allarme quando determinate condizioni di tempo sono soddisfatte. Questa funzione è utile in applicazioni come sveglie o controlli di temporizzazione.

Temperatura integrata

Il DS3231 dispone anche di un sensore di temperatura integrato. Questo sensore può essere utilizzato per monitorare la temperatura ambiente ed è particolarmente utile quando la precisione della temperatura è importante per un’applicazione.

Bassa alimentazione e backup batteria

Per preservare la precisione del tempo anche in caso di interruzione dell’alimentazione principale, il DS3231 può essere alimentato da una batteria tampone. Questa batteria garantisce che il dispositivo continui a funzionare e a tenere traccia del tempo anche quando l’alimentazione principale è interrotta.

Applicazioni comuni

Il DS3231 è ampiamente utilizzato in una vasta gamma di applicazioni, tra cui:

  1. Orologi in Tempo Reale (RTC): è comunemente utilizzato per aggiungere capacità di marcatura temporale precisa a dispositivi come orologi digitali.
  2. Sveglie digitali: il DS3231 può essere impiegato per creare sveglie precise che non devono essere resettate frequentemente.
  3. Controllo di dispositivi temporizzati: è utile in applicazioni che richiedono attivazioni o disattivazioni programmate.
  4. Data logger: può essere utilizzato per annotare i dati con marcature temporali in progetti di registrazione dati.
  5. Sistemi di automazione domestica: può essere integrato in sistemi di automazione domestica per programmare azioni basate sull’orario.

In sintesi, il DS3231 è un componente altamente affidabile e preciso per il monitoraggio del tempo e della data in applicazioni elettroniche. La sua interfaccia I2C semplifica l’integrazione con una varietà di dispositivi, rendendolo una scelta popolare per progetti basati su microcontrollori.

Il modulo micro SD card

Il modulo micro SD card è un componente elettronico progettato per essere utilizzato con schede Arduino e altre piattaforme di sviluppo compatibili. Questo modulo consente di leggere e scrivere dati su schede di memoria Micro SD e Micro SDHC (TransFlash) utilizzando la comunicazione SPI (Serial Peripheral Interface). Quello utilizzato in questo articolo è prodotto dall’azienda AZDelivery.

Ecco una descrizione dettagliata di questo modulo:

Supporto per Micro SD e Micro SDHC

Questo modulo è compatibile sia con le schede Micro SD che con le schede Micro SDHC, consentendo l’utilizzo di schede con capacità fino a 32 GB. Le schede Micro SD sono comunemente disponibili e offrono un’ampia capacità di archiviazione per dati come file di log, immagini, audio o qualsiasi altra informazione che desideri registrare.

Facile da utilizzare

Il Modulo Lettore SD è facile da integrare nei tuoi progetti Arduino. Viene fornito con una libreria Arduino preinstallata, che semplifica notevolmente la lettura e la scrittura dei dati sulle schede micro SD. Questa libreria consente di accedere facilmente ai file presenti sulla scheda e di effettuare operazioni come la creazione, la lettura, la modifica e l’eliminazione dei file.

LED di stato

Il modulo è dotato di un LED di stato che indica quando il modulo è attivo e in comunicazione con il dispositivo master. Questo LED può essere utile per il debug e il monitoraggio delle operazioni di lettura/scrittura.

Applicazioni comuni

Questo modulo di lettore di schede micro SD è ampiamente utilizzato in una serie di progetti, tra cui:

  • Data logging: per registrare dati da sensori o altre fonti su una scheda micro SD per analisi future.
  • Lettura di file multimediali: per leggere file audio o immagini da schede micro SD per la riproduzione o la visualizzazione su dispositivi.
  • Progetti IoT: in progetti basati su Internet delle cose (IoT) per registrare dati ambientali o di sensori su schede micro SD.
  • Registrazione video: in sistemi di registrazione video basati su Arduino o microcontrollori simili.

Il Modulo SD card è un componente utile e pratico per progetti che richiedono la lettura e la scrittura di dati su schede di memoria Micro SD. La sua compatibilità con Arduino e altre piattaforme di sviluppo lo rende un’aggiunta preziosa per progetti che richiedono l’archiviazione e la gestione dei dati. Grazie alla sua facilità d’uso e al supporto per schede di memoria di grandi dimensioni, è una scelta popolare tra gli appassionati di elettronica e gli sviluppatori.

Realizzazione del progetto

Lo schema elettrico

Prima di realizzare il circuito vero e proprio diamo un’occhiata al pinout della board:

Pinout del NodeMCU ESP8266
Pinout del NodeMCU ESP8266

Vediamo anche i pinout degli altri componenti:

Il pinout del modulo RTC
Il pinout del modulo RTC
Il pinout del modulo micro SD card
Il pinout del modulo micro SD card

Pinout del modulo RFID PN532. Sul lato sinistro si vede il bus I2C, sul lato superiore il bus SPI
Pinout del modulo RFID PN532. Sul lato sinistro si vede il bus I2C, sul lato superiore il bus SPI

Vediamo ora lo schema elettrico del progetto, realizzato come al solito con Fritzing:

Progetto completo
Progetto completo

È possibile che qualche modulo abbia bisogno di qualche connettore e quindi si renda necessario fare qualche saldatura. Se sei nuovo a questo argomento ti consiglio di dare una lettura all’articolo Un altro tutorial su come saldare.

Come puoi osservare, sia il modulo micro SD card che il modulo relè sono alimentati a 5V dal terminale Vin mentre il modulo RTC, come anche il sensore RFID, è alimentato a 3.3V dal terminale 3V3 della ESP8266.

Il modulo micro SD card è collegato alla porta SPI della ESP8266 che impiega i GPIO:

  • D8 per il terminale CS (Chip Select)
  • D7 per il terminale MOSI (Master Out Slave In)
  • D6 per il terminale MISO (Master In Slave OUT)
  • D5 per il terminale SCK (Serial Clock)

Il modulo RTC e il modulo RFID sono collegati alla porta I2C della ESP8266 che impiega i pin:

  • D1 per il terminale SCL
  • D2 per il terminale SDA

L’ingresso di controllo del modulo relè IN1 è collegato al pin D3 mentre i due pin Vin e GND del NodeMCU ESP8266 vengono utilizzati per alimentare il modulo a doppio relè, collegando il pin Vin (lato NodeMCU) al pin VCC (lato modulo a doppio relè) e i due pin GND (massa).

Noterai che sul modulo relè è presente un ponticello (disegnato in azzurro sul connettore sinistro) che collega i morsetti JD-VCC e VCC. Questo ponticello viene utilizzato per alimentare il modulo relè attraverso i terminali VCC e GND sul connettore destro. Senza questo ponticello, saremmo costretti ad alimentare il modulo con un alimentatore esterno.

I LED sono collegati alla ESP8266 tramite dei resistori per limitare la corrente che li attraversa ed evitare di bruciarli (e di bruciare le uscite digitali a cui sono collegati). Quello rosso sarà collegato al resistore da 100Ω, quello verde al resistore da 82Ω.

Il LED ha due terminali (chiamati anodo e catodo) e, come tutti i diodi, è un componente che ha una sua polarità: fa passare la corrente quando è polarizzato direttamente (cioè la tensione all’anodo è maggiore di quella al catodo) e blocca la corrente quando è polarizzato inversamente (cioè la tensione all’anodo è minore di quella al catodo). La tensione tra anodo e catodo, che indicheremo con Vd, varia a seconda del colore della luce emessa. In particolare abbiamo che:

  • Vd = 1.8 V per il LED rosso
  • Vd = 1.9 V per il LED giallo
  • Vd = 2 V per il LED verde
  • Vd = 2 V per il LED arancio
  • Vd = 3 V per il LED blu
  • Vd = 3 V per il LED bianco

Come facciamo ad identificare l’anodo e il catodo del LED? Lo facciamo osservando i suoi terminali. Il più lungo corrisponde all’anodo. Inoltre il corpo del LED presenta un appiattimento in un punto del bordo che indica che il terminale vicino è il catodo.

Quindi, se un LED non si accende è possibile che sia stato collegato al contrario. In questo caso, per farlo funzionare, è sufficiente invertirne i collegamenti.

Come si calcola la resistenza da collegare al LED?

Nota Bene: questo paragrafo tratta il calcolo della resistenza di limitazione in maniera teorica e richiede un minimo di conoscenza delle basi dell’Elettrotecnica. Pertanto non è fondamentale per la comprensione del resto del progetto e può essere saltato dal lettore non interessato a tali aspetti teorici.

Come abbiamo già detto, il resistore tra il generico GPIO e il LED serve a limitare la corrente che attraversa il LED. Ma come possiamo calcolare il suo valore di resistenza? Ci viene in soccorso la Legge di Ohm la quale dice che la differenza di potenziale ai capi di un resistore (cioè la tensione misurata agli estremi del resistore) è proporzionale alla corrente I che lo attraversa e la costante di proporzionalità è proprio il valore di resistenza del resistore R:

V2 - V1 = RI

Nota Bene: per amor di precisione bisogna puntualizzare che mentre il resistore è il componente fisico (l’oggetto vero e proprio), la resistenza è il suo valore. Quindi è improprio (anche se accade di frequente) chiamare il resistore col termine resistenza.

Possiamo vedere la Legge di Ohm su un semplice circuito costituito da un generatore di tensione (il cerchio a sinistra) e un resistore:

Rappresentazione della Legge di Ohm
Rappresentazione della Legge di Ohm

La tensione (o differenza di potenziale) V2 – V1 impressa dal generatore di tensione sul resistore è uguale al prodotto di R per I.

Vediamo ora uno schema leggermente più complesso dove sono presenti il solito generatore di tensione, il resistore e un LED rosso:

Circuito per il calcolo del resistore di limitazione della corrente sul LED
Circuito per il calcolo del resistore di limitazione della corrente sul LED

Nel nostro caso la Vg rappresenta la tensione presente all’uscita digitale della ESP8266 quando è HIGH ed è pari quindi a 3.3V.

La Vd è la tensione ai capi del diodo (tra anodo e catodo) quando questo è polarizzato direttamente (cioè quando fa scorrere la corrente). Avendo scelto un LED rosso, sappiamo, dalla tabella precedente, che Vd = 1.8V.

Dobbiamo determinare il valore R del resistore. Abbiamo ancora una incognita: il valore della corrente I che deve scorrere nel circuito quando il pin è in stato HIGH.

Nota Bene: quando il pin digitale è nello stato LOW la sua tensione (cioè la Vg) è nulla, ne consegue che anche la corrente I nel circuito è nulla.

I LED in genere non sopportano correnti maggiori di 20mA, quindi imponiamo una corrente massima di 15mA per stare sul sicuro.

Per la Legge di Kirchhoff alle maglie (detta anche Legge di Kirchhoff delle tensioni) , abbiamo che:

Vg - Vr - Vd = 0

Da cui ricaviamo che:

Vr = Vg - Vd 

Passando ai valori reali, abbiamo che:

Vr = 3.3V - 1.8V

Ne risulta che:

Vr = 1.5V

Ma, per la Legge di Ohm, abbiamo che:

Vr = RI

da cui:

R = Vr / I

Sostituendo i valori reali:

R = 1.5V / 0.015A

Ne deriva un valore di R pari a 100Ω.

Seguendo un ragionamento analogo per il LED verde, avremo che

R = ((3.3V - 2V) / 0.015A) = 1.3V / 0.015A = 86.67Ω

Il valore commerciale più vicino è 82Ω. Ricalcolando la corrente avremo che questa sarà pari a circa 15.8 mA. Siamo ben dentro i limiti di sicurezza.

Come creare un bot Telegram

Telegram è un’applicazione di messaggistica istantanea e VoIP che può essere installata sul tuo smartphone (Android e iPhone) o computer (PC, Mac e Linux). Telegram ti consente di creare bot con cui il nostro dispositivo può interagire.

Creiamo ora il nostro bot!

Se non hai già Telegram, installalo e poi cerca il bot botFather. Fai clic sull’elemento visualizzato. Apparirà la seguente schermata:

Prima schermata del bot botFather
Prima schermata del bot botFather

Digita il comando /start per leggere le istruzioni:

Le istruzioni per la creazione del bot
Le istruzioni per la creazione del bot

Ora digita il comando /newbot per creare il tuo bot. Dagli un nome e uno username:

La creazione del nuovo bot
La creazione del nuovo bot

Se il tuo bot è stato creato con successo, riceverai un messaggio con un link per accedere al bot e al token del bot.
Salva il token del bot perché ti servirà in seguito affinché la board possa interagire con il bot.

Ecco come appare la schermata in cui è scritto il token del bot:

Il token del bot
Il token del bot

Chiunque conosca il nome utente del tuo bot può interagire con esso. Per filtrare i messaggi in modo da ignorare quelli che non provengono dal tuo account Telegram, devi utilizzare il tuo ID utente Telegram. Pertanto, quando il tuo bot Telegram riceve un messaggio, il nostro ESP8266 saprà se proviene da noi (e quindi lo elaborerà) o da altri (e quindi lo ignorerà). Ma…..come troviamo questo ID?

Nel tuo account Telegram, cerca IDBot e avvia una conversazione con quel bot:

La prima schermata di IDBot
La prima schermata di IDBot

Quindi digita il comando /getid e lui ti risponderà col tuo ID:

Il risultato del comando /getid
Il risultato del comando /getid

A questo punto abbiamo creato il nostro bot e abbiamo tutti gli elementi per interfacciarlo con il nostro dispositivo: lo username, il token e lo userid. Questi dati li useremo per inserirli nello sketch che vedremo più avanti.

Lo sketch

Creiamo il progetto PlatformIO

Abbiamo già visto la procedura di creazione di un progetto PlatformIO nell’articolo Come creare un progetto per NodeMCU ESP8266 con PlatformIO.

Delle librerie indicate non devi installare la DHT sensor library for ESPx by Bernd Giesecke ma devi installare la WiFiManager by tzapu e la libreria UniversalTelegramBot. Installa poi la libreria RTClib by Adafruit come indicato nella foto seguente:

Installa la libreria RTClib by Adafruit
Installa la libreria RTClib by Adafruit

Installa anche la libreria ArduinoJson by Benoit Blanchon:

Libreria ArduinoJson by Benoit Blanchon
Libreria ArduinoJson by Benoit Blanchon

la libreria Adafruit BusIO by Adafruit:

Libreria Adafruit BusIO by Adafruit
Libreria Adafruit BusIO by Adafruit

e la libreria Dictionary by Anatoli Arkhipenko

Libreria Dictionary by Anatoli Arkhipenko
Libreria Dictionary by Anatoli Arkhipenko

Ora modifica il file platformio.ini per aggiungere queste tre righe:

board_build.f_cpu = 160000000L
monitor_speed = 115200
upload_speed = 921600

e queste altre due righe alla fine:

Wire
SPI

in modo che abbia un aspetto del genere:

[env:nodemcuv2]
platform = espressif8266
board = nodemcuv2
framework = arduino
board_build.f_cpu = 160000000L
monitor_speed = 115200
upload_speed = 921600
lib_deps = 
	arkhipenko/Dictionary@^3.5.0
    adafruit/RTClib@^2.1.3
	adafruit/Adafruit BusIO@^1.15.0
	bblanchon/ArduinoJson@^7.0.2
	tzapu/WiFiManager@^0.16.0
	witnessmenow/UniversalTelegramBot@^1.3.0
    WIRE
	SPI

Una volta scaricate le librerie, devi scaricare un altro pacchetto (zip) di librerie che trovi al link sottostante, scompattarle e copiare le cartelle risultanti dalla decompressione nella cartella lib del progetto PlatformIO:

in modo che il contenuto della cartella lib sia questo:

Le librerie PN532 copiate nella cartella lib del progetto PlatformIO
Le librerie PN532 copiate nella cartella lib del progetto PlatformIO

Ovviamente puoi scaricare il progetto dal link seguente:

Il progetto contiene già le librerie nella cartella lib.

Sostituisci il file main.cpp del progetto che hai creato con quello presente nel file zip.

Vediamo ora come funziona lo sketch.

Inizialmente vengono incluse le librerie necessarie:

#include <Arduino.h>
#include <ESP8266WebServer.h>
#include <ArduinoJson.h>
#include <WiFiManager.h>
#include <WiFiClientSecure.h>
#include <UniversalTelegramBot.h>   
#include <Dictionary.h>
#include <Wire.h>
#include <RTClib.h> 
#include <SPI.h> 
#include <SD.h>
#include <PN532_I2C.h>
#include <PN532.h>
#include <NfcAdapter.h>

Qui dobbiamo inserire i parametri token e Telegram User ID (che nel codice è chiamato CHAT_ID) che abbiamo ottenuto nel passaggio precedente quando abbiamo creato il bot di Telegram:

// Initialize Telegram BOT
#define BOTtoken "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY"  // your Bot Token (Get from Botfather)

// Use @myidbot to find out the chat ID of an individual or a group
// Also note that you need to click "start" on a bot before it can
// message you
#define CHAT_ID "1111111111111111"

#ifdef ESP8266
  X509List cert(TELEGRAM_CERTIFICATE_ROOT);
#endif

WiFiClientSecure client;
UniversalTelegramBot bot(BOTtoken, client);

Vengono poi definite le variabili che determinano le tempistiche di lettura dei messaggi dal bot e di lettura dei tag RFID, il server ntp e i parametri per il suo funzionamento:

// Checks for new messages every 1 second.
int botRequestDelay = 1000;
unsigned long lastTimeBotRan;

// Checks for new RFID tag every 5 seconds.
unsigned long measureDelay = 5000;                
unsigned long lastTimeRan;

const char* ntpServer = "pool.ntp.org";

const long  gmtOffset_sec = 0;
const int   daylightOffset_sec = 3600;

Vengono poi definiti gli oggetti pn532_i2c e nfc che gestiscono il lettore RFID e il dizionario dictUsers che servirà ad associare ciascun utente ai suoi privilegi. Per come è impostato, archivierà al massimo dieci coppie chiave-valore. Tale valore dovrà essere calibrato a seconda dell’occupazione di memoria:

PN532_I2C pn532_i2c (Wire) ;
NfcAdapter nfc = NfcAdapter (pn532_i2c);

Dictionary &dictUsers = *(new Dictionary(10));

Sono poi definiti il file (logFile) che registrerà gli accessi, il chip select per il modulo SD card, il path base in cui verranno creati i file di log, e i GPIO a cui saranno collegati il relè e i LED:

// set up variables using the SD utility library functions:
File logFile;
const int chipSelectSD = D8;
String pathbase = "templog/";
const int relayPin = D3;
const int greenredLed = D4;

Il file system verrà poi organizzato in cartelle e file che derivano dall’anno, mese e giorno corrente. Ad esempio il percorso templog/24/01/26 indica il giorno 26 gennaio 2024 e questo file conterrà le registrazioni di quel singolo giorno. La creazione di cartelle e file viene eseguita automaticamente dal datalogger. Se non c’è una cartella, viene creata se necessario (ad esempio se ci si sposta da gennaio a febbraio, il datalogger creerà la cartella 02 all’interno della cartella 24 e i file 01, 02, 03…… all’interno della cartella 02 , che rappresentano i giorni di febbraio). Quando l’anno cambia, passando dal 2024 al 2025, il datalogger creerà la cartella 25 allo stesso livello della cartella 24 e così via. In questo modo i file sono ordinati e facilmente rintracciabili.

Si definiscono poi l’oggetto authFile che conterrà le autorizzazioni e il suo path, seguono alcune variabili di appoggio e poi la definizione dell’oggetto rtc che gestirà il modulo orologio:

File authFile;
String pathAuth = "auth.txt";

String uuidCode = "";
String temp = "";
String readFile = "";

RTC_DS3231 rtc;

Segue poi la definizione del webserver per le API REST che rimane in ascolto sulla porta 80 e le definizioni del documento Json e del buffer che serviranno per mandare dati strutturati o riceverli da e verso le API REST:

// Web server running on port 80
ESP8266WebServer server(80);
StaticJsonDocument<1024> jsonDocument;
char buffer[1024];

La funzione addJsonObject crea le coppie chiave valore da inserire nel Json (documento Json che poi sarà la response di una API):

void addJsonObject(char *name, float value) {
  JsonObject obj = jsonDocument.createNestedObject();
  obj["name"] = name;
  obj["value"] = value;
}

La funzione splitString divide una stringa sul separatore e prende l’elemento indicato dall’indice (ci servirà per trattare i comandi provenienti dal bot Telegram):

// Splits a string on the separator and takes the element indicated by index
String splitString(String data, char separator, int index)
{
  int found = 0;
  int strIndex[] = {
    0, -1  };
  int maxIndex = data.length()-1;

  for(int i=0; i<=maxIndex && found<=index; i++){
    if(data.charAt(i)==separator || i==maxIndex){
      found++;
      strIndex[0] = strIndex[1]+1;
      strIndex[1] = (i == maxIndex) ? i+1 : i;
    }
  }
  return found>index ? data.substring(strIndex[0], strIndex[1]) : "";
}

La funzione handleNewMessages gestisce i comandi provenienti dal bot Telegram:

void handleNewMessages(int numNewMessages) {
  for (int i=0; i<numNewMessages; i++) {
    // Chat id of the requester
    String chat_id = String(bot.messages[i].chat_id);
    if (chat_id != CHAT_ID){
      bot.sendMessage(chat_id, "Unauthorized user", "");
      continue;
    }
    
    // Print the received message
    String text = bot.messages[i].text;
    Serial.println(text);

    String from_name = bot.messages[i].from_name;

    if (text == "/help") {

      String welcome = "Welcome, " + from_name + ".\n";
      welcome += "Use the following commands to control your outputs:\n\n";
      welcome += "/addmod:uuid:permission adds a user with his privileges (g or d) or modifies the privileges of an already existing user\n";
      welcome += "/del:uuid removes an user\n";
      welcome += "/readuser:uuid returns the privileges of the given user\n";
      bot.sendMessage(chat_id, welcome, "");
    }     
    else if (splitString(text, ':', 0) == "/addmod") {                        
        String uuid = splitString(text, ':', 1);
        String auth = splitString(text, ':', 2);
        if((auth == "g") || (auth == "d")) {
            dictUsers(uuid, auth);
            SD.remove(pathAuth);
            int dictEntriesNumber = dictUsers.count();
            logFile = SD.open(pathAuth, FILE_WRITE);

            // if the file opened okay, write to it:
            if (logFile) {
              for(int i = 0; i < dictEntriesNumber; i++) {            
                  String key = dictUsers.key(i);
                  String keyTrim = key;
                  keyTrim.trim(); 
                  String value = dictUsers[key];
                  value.trim();
                  if(keyTrim != "") {
                    String log = key + "," + value;
                    logFile.println(log);
                  }
                  
              }    
              // close the file:
              logFile.close();
            } 
            bot.sendMessage(chat_id, "Added/modified user " + uuid + " with permission " + auth);
        } else {
          bot.sendMessage(chat_id, "Wrong auth parameter. Must be g or d");
        }
    } 
    else if (splitString(text, ':', 0) == "/del") {                        
          String uuid = splitString(text, ':', 1);
          dictUsers.remove(uuid);
          SD.remove(pathAuth);
          int dictEntriesNumber = dictUsers.count();
          logFile = SD.open(pathAuth, FILE_WRITE);

          // if the file opened okay, write to it:
          if (logFile) {
            for(int i = 0; i < dictEntriesNumber; i++) {              
                String key = dictUsers.key(i);
                String keyTrim = key;
                keyTrim.trim(); 
                String value = dictUsers[key];
                value.trim();
                if(keyTrim != "") {
                  String log = key + "," + value;
                  logFile.println(log);
                }                
            }    
            // close the file:
            logFile.close();
          } 
        bot.sendMessage(chat_id, "Removed " + uuid + " user");
    } 
    else if (splitString(text, ':', 0) == "/readuser") {                        
        String uuid = splitString(text, ':', 1);
        String auth = dictUsers[uuid];
        bot.sendMessage(chat_id, uuid + " " + auth);
    } 
 else {
       bot.sendMessage(chat_id, "Unrecognized message. Please retry...", "");
    }

    
  }
}

Se viene digitato il comando /help vengono mostrati i comandi disponibili (già esaminati nel paragrafo che illustra il set di comandi disponibili su Telegram).

La funzione getDate crea un Json contenente la data e l’ora correnti in modo da controllare l’esattezza del modulo RTC. Il documento Json da essa creato viene usato come response per l’API getDate:

void getDate() {

  DateTime now = rtc.now(); // Read the date and time from the DS3231    

  jsonDocument.clear(); // Clear json buffer
  addJsonObject("year", now.year());
  addJsonObject("month", now.month());
  addJsonObject("day", now.day());
  addJsonObject("hour", now.hour());
  addJsonObject("minutes", now.minute());
  addJsonObject("seconds", now.second());
  serializeJson(jsonDocument, buffer);
  server.send(200, "application/json", buffer);
}

La funzione sanitize fa sì che i valori di mesi, giorni, ore, minuti e secondi abbiano sempre due cifre, anteponendo uno zero nel caso di numeri ad una cifra. Così “1” diventa “01” e così via:

String sanitize(String hms) {
  if(hms.length() < 2) {
    hms = "0" + hms;
  }
  return hms;
}

La funzione fileIsPresent controlla che un dato file di cui si fornisce il path sia presente o meno nel file system:

bool fileIsPresent(String path) {
  return SD.exists(path);
}

Viene per esempio usata dall’API fileExists.

La funzione delUser serve a rimuovere un dato utente eliminandolo prima dal dizionario e aggiornando poi il file auth.txt. Questa funzione risponde all’API delUser:

void delUser() {
  Serial.println("delUser API");
  if (server.hasArg("plain") == false) {
  //handle error here
  }
  String body = server.arg("plain");
  deserializeJson(jsonDocument, body);
  String uuid = jsonDocument["uuid"];

  dictUsers.remove(uuid);

  SD.remove(pathAuth);
  int dictEntriesNumber = dictUsers.count();
  logFile = SD.open(pathAuth, FILE_WRITE);

  // if the file opened okay, write to it:
   if (logFile) {
    for(int i = 0; i < dictEntriesNumber; i++) {
      
        String key = dictUsers.key(i);
        String keyTrim = key;
        keyTrim.trim(); 
        String value = dictUsers[key];
        value.trim();
        if(keyTrim != "") {
          String log = key + "," + value;
          logFile.println(log);
        }
        
    }    
    // close the file:
    logFile.close();
  } 
  // Respond to the client
  server.send(200, "application/json", "{}");
}

La funzione addModUser aggiunge un utente con i suoi privilegi oppure modifica i privilegi di un utente già esistente. Come la precedente funzione, aggiorna prima il dizionario e poi il file auth.txt. Questa funzione risponde all’API addModUser:

void addModUser() {
  Serial.println("addModUser API");
  if (server.hasArg("plain") == false) {
  //handle error here
  }
  String body = server.arg("plain");
  deserializeJson(jsonDocument, body);


  String uuid = jsonDocument["uuid"];
  String auth = jsonDocument["auth"];

  dictUsers(uuid, auth);
  SD.remove(pathAuth);
  int dictEntriesNumber = dictUsers.count();
  logFile = SD.open(pathAuth, FILE_WRITE);

  // if the file opened okay, write to it:
   if (logFile) {
    for(int i = 0; i < dictEntriesNumber; i++) {
      
        String key = dictUsers.key(i);
        String keyTrim = key;
        keyTrim.trim(); 
        String value = dictUsers[key];
        value.trim();
        if(keyTrim != "") {
          String log = key + "," + value;
          logFile.println(log);
        }
        
    }    
    // close the file:
    logFile.close();
  }  

  // Respond to the client
  server.send(200, "application/json", "{}");
}

La funzione setDate prende in ingresso un Json contenente anno, mese, giorno, ore, minuti e secondi e regola l’orologio con questi dati. Questo nel caso volessimo regolare l’orologio manualmente. Tale funzione viene chiamata dall’API setDate:

void setDate() {
  Serial.println("setDate API");
  if (server.hasArg("plain") == false) {
  //handle error here
  }
  String body = server.arg("plain");
  deserializeJson(jsonDocument, body);


  String year_s = jsonDocument["year"];
  String month_s = jsonDocument["month"];
  String day_s = jsonDocument["day"];
  String hour_s = jsonDocument["hour"];
  String minutes_s = jsonDocument["minutes"];
  String seconds_s = jsonDocument["seconds"];
  rtc.adjust(DateTime(year_s.toInt(), month_s.toInt(), day_s.toInt(), hour_s.toInt(), minutes_s.toInt(), seconds_s.toInt()));

  // Respond to the client
  server.send(200, "application/json", "{}");
}

La funzione fileExists controlla l’esistenza di un determinato file a partire dal path creato con l’anno, il mese e il giorno. Essa si avvale della funzione fileIsPresent incontrata precedentemente ed è chiamata dall’API fileExists:

void fileExists() {
  Serial.println("fileExists API");
  if (server.hasArg("plain") == false) {
  //handle error here
  }
  String body = server.arg("plain");
  deserializeJson(jsonDocument, body);

  String year = jsonDocument["year"];
  String month = jsonDocument["month"];
  String day = jsonDocument["day"];

  String path = "templog/" + year + "/" + month + "/" + day;
  String res = "";
  if(fileIsPresent(path)) {
    res = "true";
  } else {
    res = "false";
  }

  // Respond to the client
  String response = "{\"isPresent\" : " + res + "}";
  server.send(200, "application/json", response);
}

La funzione readfile legge il file presente al path dato e lo memorizza nella variabile readFile. Viene chiamata dall’API fileRead:

void readfile(String path) {
  readFile = "";
  char patharray[path.length() + 1];
  path.toCharArray(patharray, path.length() + 1);
  //SD.begin(chipSelectSD);
  logFile = SD.open(patharray);
  if (logFile) {
    // read from the file until there's nothing else in it:
    for (unsigned long i = 0; i <=  logFile.size() - 1; i++) {
      logFile.seek(i);
      readFile = readFile + (char)logFile.peek();
    }
    // close the file:  
    logFile.close();  
  }  
}

La funzione fileRead è l’API vera e propria che restituisce il contenuto del file letto dalla funzione readfile:

void fileRead() {
  Serial.println("fileRead API");
  if (server.hasArg("plain") == false) {
  //handle error here
  }
  String body = server.arg("plain");
  deserializeJson(jsonDocument, body);

  String year = jsonDocument["year"];
  String month = jsonDocument["month"];
  String day = jsonDocument["day"];

  String path = "templog/" + year + "/" + month + "/" + day;
  if(fileIsPresent(path)) {
    readfile(path);
    String response = "";
    readFile.trim();
    response +=   readFile;
    server.send(200, "application/json", response); 
  } else {
    server.send(404, "application/json", "{}");
  }

}

La funzione readAuth legge il file auth.txt. Questa funzione risponde all’API readAuth:

void readAuth() {
  Serial.println("readAuth API");
  if (server.hasArg("plain") == false) {
  //handle error here
  }
  String body = server.arg("plain");
  deserializeJson(jsonDocument, body);
  
  String path = pathAuth;
  String res = "";
  if(fileIsPresent(path)) {    
    readfile(path);
    String response = "";
    readFile.trim();
    response +=   readFile;
    //response += "\"}";    
    server.send(200, "application/json", response); 
  } else {
    server.send(404, "application/json", "{}");
  }

}

La funzione setupApi associa le 7 API /getDate, /readAuth, /fileExists, /fileRead, /setDate, /addModUser, /delUser alle rispettive funzioni getDate, readAuth, fileExists, fileRead, setDate, addModUser e delUser:

void setupApi() {
  server.on("/getDate", getDate);   // gets current date and hour
  server.on("/readAuth", readAuth);   // reads the permissions file
  server.on("/fileExists", HTTP_POST, fileExists); // check if the file exists
  server.on("/fileRead", HTTP_POST, fileRead); // reads the file
  server.on("/setDate", HTTP_POST, setDate);   // sets date and hour
  server.on("/addModUser", HTTP_POST, addModUser); // adds a user with his privileges or modifies the privileges of an already existing user
  server.on("/delUser", HTTP_POST, delUser); //  deletes a user with his privileges

  // start server
  server.begin();
}

La funzione updatefile crea, se non presente, il file col path corretto ricavato dalla data corrente e scrive l’UUID dell’utente che ha cercato di fare l’accesso concatenandolo con l’ora, i minuti e i secondi correnti e i privilegi di quell’utente (“g” o “d”) . Se il file è invece già presente si limita a scrivere i nuovi dati (append) su di esso:

void updatefile(DateTime now) {
  String path_folder = pathbase + sanitize(((String)now.year()).substring(2,5)) + "/" + sanitize((String)now.month()) + "/";
  String path_file = path_folder + sanitize((String)now.day()); 
  String auth = dictUsers[uuidCode];
  auth.trim();
  if(auth == "") {
    auth = "d";
  }
  String line = uuidCode + "," + sanitize((String)now.hour()) + ":" + sanitize((String)now.minute()) + ":" + sanitize((String)now.second()) + "," + auth;
  SD.begin(chipSelectSD);
  char path_folder_array[path_folder.length() + 1];
  path_folder.toCharArray(path_folder_array, path_folder.length() + 1);
  char path_file_array[path_file.length() + 1];
  path_file.toCharArray(path_file_array, path_file.length() + 1);
  
  if(!SD.exists(path_folder_array)) {SD.mkdir(path_folder_array);}  

  logFile = SD.open(path_file_array, FILE_WRITE);


  // if the file opened okay, write to it:
  if (logFile) {
    logFile.println(line);
    // close the file:
    logFile.close();
  } 
}

La funzione setup comincia con l’inizializzazione della porta seriale, l’inizializzazione del modulo Wire (con il setting del suo clock alla velocità massima consentita), l’inizializzazione dei moduli rtc e SPI:

Serial.begin (115200);
delay(2000);

Wire.setClock(400000);  // https://www.arduino.cc/reference/en/language/functions/communication/wire/setclock/
Wire.begin();
rtc.begin(); 
SPI.begin();   

Segue poi l’inizializzazione del modulo SD card:

// Initialize SPI communication with the SD module
if (!SD.begin(chipSelectSD)) {
Serial.println("Error initializing the SD card.");
return;
} 
Serial.println("SD card initialized successfully.");

e la lettura del file auth.txt, il cui contenuto verrà caricato nel dizionario:

  authFile = SD.open("auth.txt");
  if (authFile) {
  Serial.println("Reading auth file");

  // Reads the auth.txt file and populates the access permission dictionary
  while (authFile.available()) {
      char ch = authFile.read(); // read characters one by one from Micro SD Card
      if(ch == '\n') {
        dictUsers(splitString(temp, ',', 0), splitString(temp, ',', 1));
        temp = "";
      } else {
      temp += ch;
      }
    }
    authFile.close();
    } else {
      Serial.println("Error reading auth file");
    }

In caso l’orologio non sia regolato, viene regolato tramite l’ora inviata dal computer durante il caricamento dello sketch. Viene poi inizializzato il lettore RFID:

// Set the date and time only once if the DS3231 has not been previously initialized
if (rtc.lostPower()) {
Serial.println("RTC not initialized. I set the time...");
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
Serial.println ("NDEF Reader");

nfc.begin ();

Segue poi la parte di gestione del collegamento WiFi:

WiFi.mode(WIFI_STA); // explicitly set mode, esp defaults to STA+AP
// it is a good practice to make sure your code sets wifi mode how you want it.

//WiFiManager, Local intialization. Once its business is done, there is no need to keep it around
WiFiManager wm;

// reset settings - wipe stored credentials for testing
// these are stored by the esp library
// wm.resetSettings();

// Automatically connect using saved credentials,
// if connection fails, it starts an access point with the specified name ( "AutoConnectAP"),
// if empty will auto generate SSID, if password is blank it will be anonymous AP (wm.autoConnect())
// then goes into a blocking loop awaiting configuration and will return success result

bool res;
// res = wm.autoConnect(); // auto generated AP name from chipid
// res = wm.autoConnect("AutoConnectAP"); // anonymous ap
res = wm.autoConnect("AutoConnectAP","password"); // password protected ap

if(!res) {
    Serial.println("Failed to connect");
    ESP.restart();
} 
else {
    //if you get here you have connected to the WiFi    
    Serial.println("Connected...yeey :)");
}

Viene poi chiamata la funzione setupApi() che serve ad instradare le varie richieste REST verso le funzioni appropriate (come già visto poco più sopra).

Si inizializza come OUTPUT il pin che comanda il relè e inizializzato al valore HIGH:

  pinMode(relayPin, OUTPUT);
  digitalWrite(relayPin, HIGH);

Viene poi ricevuto l’UTC time dal server NTP, aggiunti i certificati per il funzionamento del bot Telegram e inviati i messaggi iniziali al bot:

#ifdef ESP8266
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);      // get UTC time via NTP
client.setTrustAnchors(&cert); // Add root certificate for api.telegram.org
#endif

bot.sendMessage(CHAT_ID, "Hi! I'm online!", "");
bot.sendMessage(CHAT_ID, "Ready to operate. Type /help to see the command list.", "");

Infine, si inizializza come OUTPUT il pin che comanda i LED e inizializzato al valore LOW in modo che si accenda il solo LED rosso:

pinMode(greenredLed, OUTPUT);
digitalWrite(greenredLed, LOW);

La funzione loop inizia con la chiamata all’handler delle API REST:

server.handleClient();

Continua con un blocco che ogni botRequestDelay ms controlla se sono arrivati comandi dal bot Telegram e li manda, per gestirli, alla funzione handleNewMessages, che abbiamo già incontrato:

if (millis() > lastTimeBotRan + botRequestDelay)  {
        int numNewMessages = bot.getUpdates(bot.last_message_received + 1);
        while(numNewMessages) {
          Serial.println("got response");
          handleNewMessages(numNewMessages);
          numNewMessages = bot.getUpdates(bot.last_message_received + 1);
        } 
        lastTimeBotRan = millis(); 
    }  

Infine troviamo il blocco che ogni measureDelay ms rileva la presenza di un tag RFID nelle vicinanze del lettore, ne legge l’UUID e ricava i suoi permessi dal dizionario. In caso di accesso “granted” accende il LED verde, fa scattare il relè, manda un messaggio di avviso al bot, aspetta 5 secondi, spegne il LED verde e accende quello rosso, fa scattare nuovamente il relè in posizione di riposo. In caso di accesso “denied” lascia acceso il LED rosso e avvisa del tentativo di accesso mandando un messaggio al bot:

if (millis() > lastTimeRan + measureDelay)  {
          if (nfc.tagPresent (100))
          {
              DateTime now = rtc.now();
              NfcTag tag = nfc.read ();
              uuidCode = tag.getUidString();
              uuidCode.replace(" ", "");
              updatefile(now);          
              String auth = dictUsers[uuidCode];
              Serial.println("UUID: " + uuidCode);
              Serial.println("auth: " + auth);
              auth.trim();
              if(auth == "g") {
                digitalWrite(greenredLed, HIGH);
                Serial.println("access granted");
                Serial.println("opening the door");
                bot.sendMessage(CHAT_ID, "Granted access to user " + uuidCode + " at " +  sanitize((String)now.day()) + "/" + sanitize((String)now.month()) + "/" + sanitize((String)now.year())   +  " "  + sanitize((String)now.hour()) + ":" + sanitize((String)now.minute()) + ":" + sanitize((String)now.second()));
                digitalWrite(relayPin, LOW);
                delay(5000);
                digitalWrite(relayPin, HIGH);
                Serial.println("closing the door");
                digitalWrite(greenredLed, LOW);
              } else {
                digitalWrite(greenredLed, LOW);
                bot.sendMessage(CHAT_ID, "Denied access to user " + uuidCode + " at " +  sanitize((String)now.day()) + "/" + sanitize((String)now.month()) + "/" + sanitize((String)now.year())   +  " "  + sanitize((String)now.hour()) + ":" + sanitize((String)now.minute()) + ":" + sanitize((String)now.second()));
                Serial.println("access denied");
              }
              uuidCode = "";    
          }         
    lastTimeRan = millis(); 
  }  

Come connettere la board ad Internet

Dopo aver caricato lo sketch sulla board, apri il Serial Monitor per vedere i messaggi provenienti dal dispositivo.

NOTA IMPORTANTE: dopo il caricamento dello sketch sulla ESP8266 può capitare che il sensore RFID non venga riconosciuto. Ciò provoca un crash della ESP8266 con relativo riavvio della stessa in un loop infinito. Per rimediare è sufficiente staccare il filo di alimentazione del lettore RFID e riattaccarlo. In questo modo, al successivo riavvio della ESP8266 tale lettore verrà riconosciuto e il nostro microcontrollore proseguirà con il suo normale setup.

Per prima cosa la board va in modalità Access Point e ci fornirà un indirizzo IP che useremo a breve. Questa operazione serve per connettere la board ad Internet senza dover inserire nel codice i parametri della rete WiFi (SSID e password).

La board ci fornisce il suo indirizzo IP
La board ci fornisce il suo indirizzo IP

In questo caso l’indirizzo IP è 192.168.4.1.

A questo punto la ESP8266 è in modalità Access Point (con SSID AutoConnectAP) e dobbiamo connettere il nostro computer alla rete AutoConnectAP. Se andiamo nel menu delle reti del nostro computer, dovremmo vedere anche la rete AutoConnectAP nell’elenco delle reti wireless.

Lista delle reti WiFi disponibili
Lista delle reti WiFi disponibili

Connetti il computer alla rete AutoConnectAP. Quindi vai sul tuo browser e inserisci l’IP precedentemente fornito dalla ESP8266 (che in questo esempio è 192.168.4.1)

Vedrai una schermata come questa:

La schermata del browser per scegliere la rete
La schermata del browser per scegliere la rete

Clicca il bottone ConfigureWiFi. Ti mostrerà le reti disponibili:

Lista delle reti disponibili
Lista delle reti disponibili

Scegli la SSID della tua rete:

Scegli la tua rete
Scegli la tua rete

Inserisci la password della tua rete e clicca il bottone save:

Inserisci la password
Inserisci la password

La risposta della board
La risposta della board

Il modulo ESP8266 conserva memorizzati i parametri di accesso anche se lo spegni, li ricorderà al riavvio e si ricollegherà automaticamente senza dover ripetere questa procedura. Solo se lo resetti rimuovendo il commento da questa riga

// wm.resetSettings();

perderà i parametri di connessione.

Fra i vari messaggi che verranno stampati sul Serial Monitor, la libreria WiFiManager ci indicherà anche quale IP le è stato assegnato dal modem WiFi.

Nota Bene: il dispositivo può memorizzare solo una rete. Se successivamente lo colleghi a un’altra rete, dimenticherà le impostazioni della rete precedente.

Testiamo il progetto con le API REST

Una volta che la ESP8266 è stata connessa alla rete WiFi ci fornirà tramite il Serial Monitor di PlatformIO il suo indirizzo IP, come visibile nella figura seguente:

Ricaviamo l'IP della board
Ricaviamo l’IP della board

In questo caso l’IP assegnato dal router WiFi alla board è 192.168.1.153. Tale IP ci servirà per comporre le API REST.

NOTA: ovviamente non è detto che nel tuo caso venga assegnato lo stesso indirizzo IP alla tua board ma sarà sicuramente diverso dal mio. Negli esperimenti con Postman tu dovrai, ovviamente, usare l’IP assegnato alla tua board.

Per interagire con la board abbiamo bisogno di un software apposito che si chiama Postman. Dopo aver installato il programma, siamo pronti ad usarlo.

Ecco come si presenta la sua schermata iniziale:

Schermata iniziale di Postman
Schermata iniziale di Postman

Nella finestra principale si trova una barra in cui dovrai inserire l’API.

Alla sinistra di questa barra c’è un menù a tendina che consente di scegliere il tipo di API (per esempio GET, POST, PUT…).

In generale la struttura dell’API è di questa forma:

http://IP_ESP8266/nomeAPI

Proviamo le API GET. Nel menu a tendina a sinistra scegliamo la voce GET. Proviamo l’API getDate mettendo la riga

192.168.1.153/getDate

nella casella a destra del menu GET e pigiamo il tasto Send. Se tutto va bene otterremo una risposta come questa:

API getDate
API getDate

Ora proviamo la readAuth. Nel menu a tendina a sinistra scegliamo la voce GET. Proviamo l’API readAuth mettendo la riga

192.168.1.153/readAuth

nella casella a destra del menu GET e pigiamo il tasto Send. Se tutto va bene otterremo una risposta come questa:

API readAuth
API readAuth

Passiamo ora alle API di tipo POST. Nel menu a tendina a sinistra scegliamo il tipo POST.

Esse prevedono di fornire come dato di ingresso dell’API un documento Json. Per fornire questo Json, dovrai selezionare la voce Body che sta sotto la barra dell’URL. Poi seleziona la voce raw (sotto Body) e poi, sul menù a tendina sulla destra, seleziona la voce JSON al posto di Text.

Vediamolo con la prima API POST, la fileExists. Mettiamo la riga

192.168.1.153/fileExists

inseriamo il file Json come spiegato poco sopra e pigiamo il tasto Send.

La risposta dovrebbe essere qualcosa del genere:

API fileExists
API fileExists

Per regolare l’orologio usiamo la setDate sempre inserendo il Json come spiegato sopra. Usando la riga

192.168.1.153/setDate

dovremmo quindi avere una situazione analoga a questa:

API setDate
API setDate

Premendo il tasto Send l’orologio verrà settato con i dati del Json in ingresso.

Ora leggiamo un dato file col comando

192.168.1.153/fileRead

sempre inserendo il Json contenente l’anno, il mese e il giorno per identificarlo nel file system.

La situazione sarà di questo tipo:

API fileRead
API fileRead

Ora testiamo l’API addModUser. Il comando sarà;

192.168.1.153/addModUser
API addModUser
API addModUser

In questo caso, se l’utente 73051E99 non è già presente nel file auth.txt verrà aggiunto col suo privilegio (d), nel caso fosse già presente gli verrà semplicemente modificato il privilegio.

Vediamo l’ultima API cioè delUser, sempre di tipo POST.

In questo caso il comando sarà

192.168.1.153/delUser
API delUser
API delUser

Premendo il tasto Send, l’utente 73051E99 verrà eliminato (sia dal dizionario che dal file auth.txt).

Il video successivo mostra il funzionamento di alcune API REST con Postman (qui la board era collegata ad un diverso router che le ha assegnato l’indirizzo 192.168.1.121):

Testiamo il progetto con Telegram

Il video successivo mostra il funzionamento di alcuni comandi con Telegram:

Osservazioni finali

Lo sketch è abbastanza complesso ed include molte librerie. Quindi è sicuramente un po’ pesante da eseguire su una board del genere. Aggiungiamo il fatto che il lettore RFID non è particolarmente veloce nell’esecuzione delle sue operazioni (in particolare il comando nfc.tagPresent () rallenta in maniera significativa la funzione loop in quanto è bloccante). Tale comando prevede un timeout (per esempio nfc.tagPresent (100)) che fa in modo che la funzione non blocchi per troppo tempo l’esecuzione dello sketch. Inoltre, anche il comando bot.getUpdates(bot.last_message_received + 1) risulta piuttosto lento. Il risultato di tutto ciò è una certa lentezza nell’esecuzione delle API REST e dei comandi sul bot Telegram. Bisogna aspettare diversi secondi prima che vengano portate a termine. Si può cercare un compromesso tra i parametri botRequestDelay, measureDelay e il timeout di nfc.tagPresent () in modo da avere un comportamento più responsivo possibile.

Newsletter

Se vuoi essere aggiornato sui nuovi articoli, iscriviti alla newsletter. Prima dell’iscrizione alla newsletter leggi la pagina Privacy Policy (UE)

Se ti vuoi disiscrivere dalla newsletter clicca sul link che troverai nella mail della newsletter.

Inserisci il tuo nome
Inserisci la tua email
0 0 votes
Valutazione articolo
guest
2 Commenti
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Whitney Crist
Whitney Crist
6 mesi fa

Your blog is a breath of fresh air in the often mundane world of online content. Your unique perspective and engaging writing style never fail to leave a lasting impression. Thank you for sharing your insights with us.

2
0
Would love your thoughts, please comment.x
Torna in alto