Skip to Content

Calendario Mensile su Display E-Paper

Calendario Mensile su Display E-Paper

In questo tutorial imparerai come implementare un Calendario Mensile su un Display E-Paper usando un ESP32. Il display mostrerà anche l’ora corrente e sincronizzerà l’orario con un server di tempo internet (server SNTP), così che ora e calendario siano sempre precisi. Niente più impostazioni manuali di ora e data.

Componenti necessari

Sto usando l’ESP32 lite come microprocessore, perché è economico e ha un’interfaccia di ricarica batteria, che permette di far funzionare il calendario con una batteria LiPo. Qualsiasi altro ESP32 o ESP8266 con memoria sufficiente andrà bene, ma preferibilmente scegli uno con interfaccia di ricarica batteria. Dovresti poter usare anche un Arduino, ma non l’ho provato.

Display e-Paper da 4,2″

ESP32 lite Lolin32

ESP32 lite

USB data cable

Cavo dati USB

Dupont wire set

Set di fili Dupont

Half_breadboard56a

Breadboard

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Display e-Paper

Il display e-Paper usato in questo progetto è un modulo da 4,2″, con risoluzione 400×300 pixel, 4 livelli di grigio, tempo di aggiornamento parziale di 0,4 secondi e un controller integrato con interfaccia SPI.

Fronte e retro del modulo display e-Paper da 4,2″

Nota che il modulo ha un piccolo jumper/interruttore sul retro per passare da SPI a 4 fili a SPI a 3 fili. Qui useremo il default SPI a 4 fili, quindi non dovresti dover cambiare nulla.

Retro del modulo display con interfaccia SPI e controller

Il modulo display funziona a 3,3V o 5V, ha una corrente di sleep molto bassa di 0,01µA e consuma solo circa 26,4mW durante l’aggiornamento. Per maggiori informazioni sui display e-Paper in generale, dai un’occhiata ai seguenti due tutorial: Interfacing Arduino To An E-ink Display e Weather Station on e-Paper Display.

Collegamento e test del display e-Paper

Per prima cosa colleghiamo e testiamo il funzionamento del display e-Paper. L’immagine seguente mostra il cablaggio completo per alimentazione e SPI.

Connecting 4.2" e-Paper to ESP32 via SPI
Collegamento del display e-Paper da 4,2″ all’ESP32 via SPI

Di seguito una tabella con tutte le connessioni per comodità. Nota che puoi alimentare il display con 3,3V o 5V, ma l’ESP32-lite ha solo un’uscita a 3,3V e le linee dati SPI devono essere a 3,3V!

Display e-PaperESP32 lite
CS/SS5
SCL/SCK 18
SDA/DIN/MOSI23
BUSY15
RES/RST2
DC0
VCC3.3V
GNDG

Puoi usare una breadboard per collegare tutto. Io però ho collegato il display direttamente all’ESP32 con fili Dupont. L’immagine sotto mostra come era il mio setup.

ESP32 collegato con display e-Paper

Installare la libreria GxEPD2

Prima di poter disegnare o scrivere sul display e-Paper dobbiamo installare due librerie. La Adafruit_GFX libreria grafica, che fornisce un set comune di primitive grafiche (testo, punti, linee, cerchi, ecc.). E la GxEPD2 libreria, che fornisce il driver grafico per il display e-Paper.

Installa le librerie nel modo consueto. Dopo l’installazione dovrebbero apparire nel Library Manager come mostrato.

Adafruit_GFX and GxEPD2 libraries in Library Manager
Librerie Adafruit_GFX e GxEPD2 nel Library Manager

Codice di test

Una volta installata la libreria, esegui il seguente codice di test per assicurarti che tutto funzioni.

#include "GxEPD2_BW.h"

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

void setup() {
  epd.init(115200, true, 50, false);
  epd.setRotation(1);
  epd.setTextColor(GxEPD_BLACK);   
  epd.setTextSize(2);
  epd.setFullWindow();

  epd.fillScreen(GxEPD_WHITE);     
  epd.setCursor(90, 190);
  epd.print("Makerguides");  
  epd.display();
  epd.hibernate();
}

void loop() {}

Il codice stampa il testo “Makerguides” e dopo un po’ di sfarfallio del display (refresh completo) dovresti vederlo sul display come mostrato sotto:

Test output on 4.2" e-Paper display
Output di test sul display e-Paper da 4,2″

Se non funziona, c’è qualcosa che non va. Probabilmente il display non è cablato correttamente o è stato scelto il driver sbagliato. La riga critica di codice è questa, dove specifichiamo il driver per il display da 4,2″:

GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

La Readme GxEPD2 libreria elenca tutti i display supportati e puoi trovare i dettagli nei file header, ad esempio GxEPD2.h. Trova il driver specifico per il tuo display. Potrebbe servire un po’ di tentativi.

Codice per un Calendario su e-Paper

In questa sezione scriveremo il codice per il nostro Calendario. Lo screenshot sotto mostra come sarà:

Calendar view
Vista calendario

Il giorno e l’ora correnti sono mostrati in alto. Sotto il display mostra il mese corrente, l’anno e i giorni del mese con il giorno corrente evidenziato.

Di seguito il codice per questo calendario. Dai un’occhiata veloce per avere una panoramica, poi entreremo nei dettagli:

#include <WiFi.h>
#include <time.h>
#include <GxEPD2_BW.h>

#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>

const char* ssid = "SSID";
const char* password = "PWD";
const char* ntpServer = "pool.ntp.org";
const char* timezone = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const char* dayNames[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
char* dayLongNames[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char* monthNames[] = {
  "January", "February", "March", "April", "May",
  "June", "July", "August", "September", "October", "November", "December"
};
const int shifts[] = {
  3, 0, 0, 1, 0, 0, 0, 0, 0, 2,
  2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  1
};

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

bool isLeapYear(int year) {
  return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

int daysInMonth(int year, int month) {
  int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  if (month == 2 && isLeapYear(year)) {
    return 29;
  }
  return days[month-1];
}

// Return day of the week (0=Sunday, 1=Monday, ..., 6=Saturday)
int dayOfWeek(int year, int month, int day) {
  tm timeinfo = {};
  timeinfo.tm_year = year - 1900;
  timeinfo.tm_mon = month - 1;
  timeinfo.tm_mday = day;
  mktime(&timeinfo);
  return timeinfo.tm_wday;
}

void drawAt(int x, int y, const char* text, int shift, bool mark) {
  int16_t xb, yb;
  uint16_t wb, hb;
  epd.getTextBounds(text, 0, 0, &xb, &yb, &wb, &hb);
  epd.setCursor(x - wb - shift, y);
  epd.setTextColor(mark ? GxEPD_WHITE : GxEPD_BLACK);
  if (mark) {
    epd.fillRect(x - wb - shift - 3, y - hb - 3, wb + 8, hb + 8, GxEPD_BLACK);
  }
  epd.print(text);
}

void drawCalendar(int year, int month, int day) {
  char buf[6];
  int dy = 25;
  int dx = 56;
  int xoff = 10;
  int yoff = 100;

  // Print name of month and the year
  epd.setPartialWindow(0, yoff - 20, 400, 300 - (yoff - 20));
  epd.fillRect(6, yoff - 20, 400 - 12, 28, GxEPD_BLACK);
  epd.setFont(&FreeSansBold12pt7b);
  epd.setTextColor(GxEPD_WHITE);
  epd.setCursor(xoff, yoff);
  epd.printf("%s %d", monthNames[month - 1], year);

  // print names of week days
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold9pt7b);
  xoff += 30;
  for (int i = 0; i < 7; i++) {
    int x = xoff + i * dx;
    int y = yoff + 35;
    drawAt(x, y, dayNames[i], 0, false);
  }

  // Print days of the month
  int days = daysInMonth(year, month);
  int firstDay = dayOfWeek(year, month, 1);
  int x = xoff;
  int y = yoff + 65;

  for (int i = 0; i < firstDay; i++) {
    x += dx;  // shift pos of first day
  }
  epd.setFont(&FreeSans9pt7b);
  for (int d = 1; d <= days; d++) {
    sprintf(buf, "%d", d);
    drawAt(x, y, buf, shifts[d - 1], d == day);
    x += dx;
    if ((firstDay + d) % 7 == 0) {
      y += dy;
      x = xoff;
    }
  }
}

void updateTime(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(10, 60);
  epd.setPartialWindow(0, 0, 400, 80);
  epd.printf("%s %02d:%02d", dayLongNames[t.tm_wday], t.tm_hour, t.tm_min);
}

void updateCalendar(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  drawCalendar(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
}

bool isNewDay() {
  struct tm t;
  getLocalTime(&t);
  return t.tm_hour == 0 && t.tm_min == 0;
}

void syncTime() {
  WiFi.begin(ssid, password);
  for (int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) {
    delay(100);
  }
  if (WiFi.status() == WL_CONNECTED)
    configTzTime(timezone, ntpServer);
}

void clearScreen() {
  epd.setFullWindow();
  epd.fillScreen(GxEPD_WHITE);
  epd.display();
}

void initEPD() {
  epd.init(115200, true, 50, false);
  epd.setRotation(0); 
}

void setup() {
  Serial.begin(115200);
  syncTime();
  initEPD();
  clearScreen();
  epd.drawPaged(updateCalendar, 0);
  epd.hibernate();
}

void loop() {
  if (isNewDay()) {
    syncTime();
    clearScreen();    
    epd.drawPaged(updateCalendar, 0);
  }
  epd.drawPaged(updateTime, 0);
  epd.hibernate();
  delay(60 * 1000);  // 60 seconds
}

Librerie

Il codice inizia includendo le librerie necessarie per la connettività WiFi, la gestione del tempo e la funzionalità del display e-Paper.

#include <WiFi.h>
#include <time.h>
#include <GxEPD2_BW.h>

Font

Poi includiamo i font usati. Puoi scegliere font diversi e finché sono della stessa dimensione (9pt, 12pt, 24pt) il layout del calendario dovrebbe funzionare. Per la lista dei font disponibili guarda here.

#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>

Costanti

Poi definiamo alcune costanti per le credenziali WiFi, il server NTP, il fuso orario, gli array per i nomi di giorni e mesi e le correzioni di layout.

const char* ssid = "SSID";
const char* password = "PWD";
const char* ntpServer = "pool.ntp.org";
const char* timezone = "AEST-10AEDT,M10.1.0,M4.1.0/3";

const char* dayNames[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
char* dayLongNames[] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
const char* monthNames[] = {
  "January", "February", "March", "April", "May",
  "June", "July", "August", "September", "October", "November", "December"
};

const int shifts[] = {
  3, 0, 0, 1, 0, 0, 0, 0, 0, 2,
  2, 2, 2, 2, 2, 2, 2, 2, 2, 0,
  0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
  1
};

Dovrai sostituire le costanti SSID e PASSWORD con quelle del tuo WiFi, e probabilmente anche il TIMEZONE del luogo in cui vivi.

La specifica del fuso orario “AEST-10AEDT,M10.1.0,M4.1.0/3” è per l’Australia, corrispondente all’Australian Eastern Standard Time (AEST) con aggiustamenti per l’ora legale. Per altre definizioni di fuso orario dai un’occhiata al Posix Timezones Database. Copia e incolla la stringa che trovi lì e modifica la costante TIMEZONE di conseguenza.

Correzioni

Le costanti per i nomi di giorni e mesi sono ovvie, ma la costante shifts richiede una spiegazione. I nomi dei giorni e i numeri dei giorni del mese nella vista calendario sono allineati a destra. Uso la funzione getTextBounds() per ottenerlo. Tuttavia, apparentemente i limiti di testo calcolati da questa funzione non sono del tutto precisi e quindi l’allineamento a destra non è perfetto. Vedi sotto:


Right alignment without (left) and with (right) shift correction
Allineamento a destra senza (sinistra) e con (destra) correzione di spostamento

La costante shifts sposta l’allineamento a destra di un certo numero di pixel per un giorno specifico, ad esempio shifts[0] sposta il giorno 1 di tre pixel a destra. L’immagine sopra confronta l’allineamento delle colonne dei giorni senza (sinistra) e con (destra) la correzione di spostamento. Si vede che i numeri dei giorni a sinistra non sono perfettamente allineati a destra, cosa che dà fastidio guardando il calendario completo. Le costanti di spostamento risolvono questo problema.

Configurazione del display

La seguente riga di codice crea l’oggetto display e lo collega ai pin CS, SCL, SDA, BUSY, RES e DC.

//CS(SS)=5, SCL(SCK)=18, SDA(MOSI)=23, BUSY=15, RES(RST)=2, DC=0
GxEPD2_BW<GxEPD2_420_GDEY042T81, GxEPD2_420_GDEY042T81::HEIGHT> epd(GxEPD2_420_GDEY042T81(5, 0, 2, 15));

Se usi un display e-Paper diverso potresti dover scegliere un driver differente. La libreria Readme GxEPD2 elenca tutti i display supportati e puoi trovare i dettagli nei file header, ad esempio GxEPD2.h e GxEPD2_display_selection_new_style.h.

Funzioni per la data

Poi ci sono alcune funzioni per la data. La funzione isLeapYear() restituisce true se l’anno year passato è bisestile.

bool isLeapYear(int year) {
  return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}

La funzione daysInMonth() restituisce il numero di giorni in un dato mese, considerando gli anni bisestili per febbraio.

int daysInMonth(int year, int month) {
  int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
  if (month == 2 && isLeapYear(year)) {
    return 29;
  }
  return days[month - 1];
}

E la funzione dayOfWeek() calcola l’indice del giorno della settimana per una data specifica. Restituisce valori da 0 (domenica) a 6 (sabato).

int dayOfWeek(int year, int month, int day) {
  tm timeinfo = {};
  timeinfo.tm_year = year - 1900;
  timeinfo.tm_mon = month - 1;
  timeinfo.tm_mday = day;
  mktime(&timeinfo);
  return timeinfo.tm_wday;
}

Funzioni di disegno

Funzione drawAt

La funzione drawAt() si occupa di disegnare testo sul display e-Paper a coordinate specificate, con evidenziazione opzionale.

void drawAt(int x, int y, const char* text, int shift, bool mark) {
  int16_t xb, yb;
  uint16_t wb, hb;
  epd.getTextBounds(text, 0, 0, &xb, &yb, &wb, &hb);
  epd.setCursor(x - wb - shift, y);
  epd.setTextColor(mark ? GxEPD_WHITE : GxEPD_BLACK);
  if (mark) {
    epd.fillRect(x - wb - shift - 3, y - hb - 3, wb + 8, hb + 8, GxEPD_BLACK);
  }
  epd.print(text);
}

L’evidenziazione serve a mettere in risalto il giorno corrente sottolineandolo con uno sfondo nero e scrivendo il testo in bianco. Qui sotto un esempio per il giorno 6:

Highlighting of the current day
Evidenziazione del giorno corrente

Nota il parametro shift, usato per correggere l’allineamento a destra dei giorni come descritto sopra. Otteniamo i limiti del testo chiamando getTextBounds(), poi spostiamo il testo a destra sottraendo la larghezza wb del testo dalla posizione di disegno x e sottraendo anche la correzione shift.

Funzione drawCalendar

La funzione drawCalendar() disegna l’intero calendario per un giorno, mese e anno specificati.

void drawCalendar(int year, int month, int day) {
  ...
  // Print name of month and the year
  epd.setPartialWindow(0, yoff - 20, 400, 300 - (yoff - 20));
  epd.fillRect(6, yoff - 20, 400 - 12, 28, GxEPD_BLACK);
  epd.setFont(&FreeSansBold12pt7b);
  epd.setTextColor(GxEPD_WHITE);
  epd.setCursor(xoff, yoff);
  epd.printf("%s %d", monthNames[month - 1], year);

  // Print names of week days
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold9pt7b);
  xoff += 30;
  for (int i = 0; i < 7; i++) {
    int x = xoff + i * dx;
    int y = yoff + 35;
    drawAt(x, y, dayNames[i], 0, false);
  }

  // Print days of the month
  int days = daysInMonth(year, month);
  int firstDay = dayOfWeek(year, month, 1);
  int x = xoff;
  int y = yoff + 65;

  for (int i = 0; i < firstDay; i++) {
    x += dx;  // shift pos of first day
  }
  epd.setFont(&FreeSans9pt7b);
  for (int d = 1; d <= days; d++) {
    sprintf(buf, "%d", d);
    drawAt(x, y, buf, shifts[d - 1], d == day);
    x += dx;
    if ((firstDay + d) % 7 == 0) {
      y += dy;
      x = xoff;
    }
  }
}

La vista calendario è essenzialmente divisa in quattro parti. La prima parte (1) in alto mostra il nome del giorno corrente e l’ora attuale. La seconda parte (2) mostra il mese e l’anno correnti. La parte (3) mostra i nomi dei giorni. E la parte (4) mostra i giorni del mese.

Components of calendar view
Parti della vista calendario

Funzione updateTime

La funzione drawCalendar() mostrata sopra disegna le parti da 2 a 4, mentre la funzione updateTime() qui sotto disegna la parte 1. Ottiene l’ora locale corrente e la visualizza.

void updateTime(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  epd.setTextColor(GxEPD_BLACK);
  epd.setFont(&FreeSansBold24pt7b);
  epd.setCursor(10, 60);
  epd.setPartialWindow(0, 0, 400, 100);
  epd.printf("%s %02d:%02d", dayLongNames[t.tm_wday], t.tm_hour, t.tm_min);
}

Funzione updateCalendar

Analogamente, la funzione updateCalendar() ottiene la data corrente e poi chiama drawCalendar() per aggiornare il calendario mostrato.

void updateCalendar(const void* pv) {
  struct tm t;
  getLocalTime(&t);
  drawCalendar(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday);
}

Nota che entrambe le funzioni di aggiornamento, updateTime() e updateCalendar() eseguono un aggiornamento parziale, che è più veloce ed evita lo sfarfallio del display. Se vuoi saperne di più sull’operazione di aggiornamento parziale, dai un’occhiata al tutorial Partial Refresh of e-Paper Display.

Sincronizzazione dell’ora

La funzione syncTime() crea una connessione WiFi e poi sincronizza l’orologio interno dell’ESP32 con il server SNTP “pool.ntp.org” chiamando configTzTime().

void syncTime() {
  WiFi.begin(ssid, password);
  for (int i = 0; i < 10 && WiFi.status() != WL_CONNECTED; i++) {
    delay(100);
  }
  if (WiFi.status() == WL_CONNECTED)
    configTzTime(timezone, ntpServer);
}

Puoi specificare altri server SNTP, o anche più server a cui connetterti. Dai un’occhiata al tutorial How to synchronize ESP32 clock with SNTP server per maggiori informazioni.

Nota che la funzione syncTime() tenta di connettersi al WiFi solo 10 volte e poi rinuncia. Questo ha il vantaggio che il calendario continua a funzionare anche se non c’è WiFi disponibile. Altrimenti il codice si bloccherebbe qui finché il WiFi non diventa disponibile.

Funzione setup

Nella funzione setup() inizializziamo la comunicazione seriale, sincronizziamo l’ora, inizializziamo il display e-Paper, puliamo lo schermo e disegniamo il calendario.

void setup() {
  Serial.begin(115200);
  syncTime();
  initEPD();
  clearScreen();
  epd.drawPaged(updateCalendar, 0);
  epd.hibernate();
}

Questo significa che ogni volta che l’ESP32 viene resettato l’ora viene sincronizzata e non devi preoccuparti di impostare manualmente ora e data.

Funzione loop

La funzione loop() controlla se è iniziato un nuovo giorno, sincronizza l’ora se è così, pulisce lo schermo e aggiorna il calendario e l’ora mostrati.

void loop() {
  if (isNewDay()) {
    syncTime();
    clearScreen();    
    epd.drawPaged(updateCalendar, 0);
  }
  epd.drawPaged(updateTime, 0);
  epd.hibernate();
  delay(60 * 1000);  // 60 seconds
}

Il calendario viene ridisegnato/aggiornato solo una volta al giorno, mentre l’ora viene aggiornata ogni 60 secondi. Tutti questi aggiornamenti sono parziali, quindi veloci e senza sfarfallio.

Tuttavia, la funzione clearScreen() che viene chiamata una volta al giorno, quando il calendario viene aggiornato, esegue un aggiornamento completo. Questo serve a evitare il “burn in” delle immagini residue degli aggiornamenti parziali. Il tutorial Waveshare Manual ha più informazioni a riguardo.

Nota che la sincronizzazione giornaliera dell’ora gestisce i cambiamenti dell’ora legale, ma ci saranno alcune ore in cui l’orologio sarà sfasato. Puoi evitare questo sincronizzando ogni 30 minuti circa. Dai un’occhiata al tutorial Digital Clock on e-Paper Display per un esempio di codice.

Conclusioni

In questo tutorial hai imparato come implementare un Calendario Mensile su un Display E-Paper usando un ESP32.

I display e-Paper consumano pochissima energia e sono quindi ottimi per progetti alimentati a batteria. Potresti far funzionare l’ESP32 con il calendario su una batteria LiPo, ma in questo caso sarebbe meglio mettere l’ESP32 in modalità deep-sleep tra un aggiornamento e l’altro del display. Non ti ho mostrato come fare in questo tutorial, ma il tutorial Digital Clock on e-Paper Display contiene codice di esempio per questo.

Inoltre, per un calendario alimentato a batteria sarebbe utile monitorare il livello di carica della batteria LiPo e mostrare un piccolo simbolo della batteria. Dai un’occhiata al tutorial Monitor Battery Levels with a MAX1704X se vuoi saperne di più sul monitoraggio della batteria.

Oltre al livello di carica potresti anche mostrare la temperatura ambiente o dati meteo. Dai un’occhiata al tutorial Weather Station on e-Paper Display per maggiori informazioni.

Se hai commenti, sentiti libero di lasciarli nella sezione commenti.

Buon divertimento con il fai-da-te ; )