Guest post created with ♥ by
Eugenio
|
LineaMeteoStazione e-ink display with esp32
Display a inchiostro con ESP32
Per avere un’opzione di visualizzazione dei dati e non solo la visualizzazione con i siti Web online, volevo qualcosa che fosse durata a lungo a batterie, ecco perché ho scelto un Display a inchiostro da 4,3 pollici (Display Ink di Good Display, ma può essere utilizzato anche Waveshare )
4.2 inch e-ink display 400×300
Ho anche usato la stessa scheda di sviluppo del dispositivo esterno perché ha un caricabatterie incluso per le batterie al litio. Ha anche un sensore BME680, che uso per monitorare la temperatura interna e l’umidità, nonché la qualità dell’aria.
La scatola per il display e’ stampata in 3D e usa una board che puoi trovare nei file di Eagle in Github.
Il display, come mostrato nella foto sotto, è incollato su un cartoncino che a sua volte è retto da delle punte color oro alla scatola. Ci sono anche le altre schede per il sensore e l’ESP32 e anche il pulsante, che serve per aggiornare manualmente il display se necessario e per resettare la scheda:
Programmare il Display per visualizzare i dati
Per raccogliere i dati ora esamineremo il codice utilizzato e come funziona il codice. Prima di tutto, devi avere l’ IDE Arduino installato almeno la versione 1.8.13 e dovrai installare dal gestore della scheda di Arduino l’ESP32 seguendo questi passaggi:
Per installare la scheda ESP32 nel tuo IDE Arduino, segui queste istruzioni seguenti:
Nel tuo IDE Arduino, vai su File > Preferenze Immettere https://dl.espressif.com/dl/package_esp32_index.json nel campo “Ulteriori URL di gestione schede”. Quindi, fare clic sul pulsante “OK”:Nota: se si dispone già dell’URL delle schede ESP8266, è possibile separare gli URL con una virgola come segue: https://dl.espressif.com/dl/package_esp32_index.json, http:/ /arduino.esp8266.com/stable/package_esp8266com_index.json Apri Gestione schede. Vai su Strumenti > Bacheca > Gestore bacheche… Cerca ESP32 e premi il pulsante di installazione per “ESP32 di Espressif Systems ” e attendi il completamento dell’installazione.
Arduino IDE esp32 additional board manager
Puoi trovare maggiori informazioni su questo articolo “Esp32: piedinatura, specifiche e configurazione dell’Arduino IDE ”.
Una volta installato, puoi programmare ESP32 con questo codice .
Per prima cosa includiamo la libreria. Queste librerie possono essere trovate nel gestore delle librerie dell’IDE di Arduino cercandole o seguendo i link sul codice, ma alcune di esse sono già preinstallate se hai installato il modulo ESP.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include
<Arduino.h>
#include
<HTTPUpdate.h>
#include
<ArduinoJson.h>
#include
<WiFi.h>
#include
"time.h"
#include
<SPI.h>
#include
<JSON_Decoder.h>
#include
<OpenWeather.h>
#include
<WiFiManager.h>
#include
<math.h>
#include
<String.h>
WiFiManager wifiManager;
#include
"NTPClient.h"
#include
<WiFiUDP.h>
#include
<FirebaseESP32.h>
#include
<GxEPD2_BW.h>
#include
<GxEPD2_3C.h>
#include
<U8g2_for_Adafruit_GFX.h>
#include
"epaper_fonts.h"
#include
"lang.h"
#include
"common_functions.h"
#include
<Adafruit_Sensor.h>
#include
"Adafruit_BME680.h"
#include
"LC709203F.h"
#include
<WebServer.h>
#include
<ESPmDNS.h>
#include
<HTTPUpdateServer.h>
#include
<WiFiClient.h>
#include
<RTClib.h>
Creiamo tutte le dichiarazioni e le variabili che ci servono per far funzionare il codice e dobbiamo anche inserire i dettagli di Firebase che sono quelli che hai ricavato dall’articolo che parla della Stazione Meteo. Puoi anche inserire i dettagli riguardo all’Ota da remoto se desideri utilizzare questa funzione che è ampiamente spiegata QUI . Avrai bisogno dell’API. Quando è richiesto un aggiornamento e desideri farlo manualmente, dovrai modificare il numero di versione nel codice che stai caricando.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
LC709203F gg;
uint8_t percentage;
float
voltage;
float
VOLTAGEOUT;
byte
PERCENTAGEOUT;
#define
ProductKey
""
#define
Version
"1.0.0.4"
#define
MakeFirmwareInfo(k, v)
"&_FirmwareInfo&k="
k
"&v="
v
"&FirmwareInfo_&"
void
update();
WebServer httpServer(
80
);
HTTPUpdateServer httpUpdater;
const
char
*
host
=
"esp32-webupdate"
;
String DEVICE3OTA;
byte
WiFiReset
=
0
;
unsigned
long
TIMEROTA;
#define
FIREBASE_HOST
""
#define
FIREBASE_AUTH
""
FirebaseData Weather;
FirebaseConfig config;
#define
NTP_OFFSET
60
*
60
#define
NTP_INTERVAL
60
*
1000
#define
NTP_ADDRESS
"pool.ntp.org"
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, NTP_ADDRESS , NTP_OFFSET, NTP_INTERVAL);
byte
SunriseHour;
byte
SunsetHour;
byte
SunriseMinute;
byte
SunsetMinute;
String DescriptionDisplay;
int
CloudsDisplay;
String VisibilityDisplay;
String api_key;
String latitude;
String longitude;
String units
=
"metric"
;
String unitsPressure;
String unitsTemperature;
String unitsRain;
String unitsWind;
String language;
String Hemisphere;
float
constantF
=
1
;
byte
constantF32
=
0
;
float
constantpressure
=
1
;
float
constantrain
=
1
;
float
constantwind
=
1
;
String SymbolTemperature
=
"°C"
;
String MeasurementUnitPressure
=
"hPa"
;
String MeasurementUnitRain
=
"mm"
;
String MeasurementUnitWind
=
"km/h"
;
boolean
justrestart
=
true
;
unsigned
long
checkopenweather
=
0
;
OW_Weather ow;
#define
SCREEN_WIDTH
400.0
#define
SCREEN_HEIGHT
300.0
#define
ENABLE_GxEPD2_display
0
enum alignment {LEFT, RIGHT, CENTER};
static
const
uint8_t EPD_BUSY
=
4
;
static
const
uint8_t EPD_CS
=
5
;
static
const
uint8_t EPD_RST
=
16
;
static
const
uint8_t EPD_DC
=
17
;
static
const
uint8_t EPD_SCK
=
18
;
static
const
uint8_t EPD_MISO
=
19
;
static
const
uint8_t EPD_MOSI
=
23
;
GxEPD2_BW<GxEPD2_420, GxEPD2_420::HEIGHT> display(GxEPD2_420(
EPD_CS,
EPD_DC,
EPD_RST,
EPD_BUSY));
U8G2_FOR_ADAFRUIT_GFX u8g2Fonts;
boolean
LargeIcon
=
true
, SmallIcon
=
false
;
#define
Large
9
#define
Small
9
#define
VerySmall
7
#define
moon
13
String WIFI;
String DEWPOINT
=
"Dew Point "
;
String UVINDEX
=
"UV Index "
;
String SOLARRADIATION
=
"Solar Radiation "
;
String HEATINDEX
=
"Heat Index "
;
String UPDATE
=
"Last Update"
;
String INSIDE
=
"INSIDE"
;
String RAIN
=
"Rain24H "
;
String RAINRATE
=
"RainRate "
;
String RAINRATEMAX
=
"RRMax "
;
String GUST
=
"GustMax "
;
String SUNSET
=
"Sunset"
;
String SUNRISE
=
"Sunrise"
;
Adafruit_BME680 bme;
float
hum_weighting
=
0.25
;
float
gas_weighting
=
0.75
;
float
hum_score, gas_score;
float
gas_reference
=
250000
;
float
hum_reference
=
40
;
int
getgasreference_count
=
0
;
int
AQIndex;
String AQI;
float
pressurehpa;
#define
c1
-
8.78469475556
#define
c2
1.61139411
#define
c3
2.33854883889
#define
c4
-
0.14611605
#define
c5
-
0.012308094
#define
c6
-
0.0164248277778
#define
c7
0.002211732
#define
c8
0.00072546
#define
c9
-
0.000003582
float
temp;
float
temperatureIN;
float
maxTemp;
float
minTemp;
float
OffsetTemp;
int
humidity;
int
humidityIN;
float
dewPoint;
float
heatIndex;
float
CalibrationUV;
float
UVindex;
float
SolarRadiation;
float
WindSpeed;
float
windchill;
float
CalDirection;
float
Gust;
unsigned
int
Offset;
float
rainrate;
float
mmPioggia;
float
rainrateMax;
unsigned
long
timeout;
bool res;
int
TIMEZONE;
long
TIMEZONEINSECONDS;
int
SleepDuration;
String FASTREFRESH;
unsigned
long
refreshcount
=
0
;
unsigned
int
refreshtime
=
0
;
Codice di Setup
Le prime operazioni che andremo a fare sono le seguenti:
inizializziamo il display chiamando InitialiseDisplay();
Questo avvierà la comunicazione SPI e avvierà i caratteri, la direzione della scrittura sul display se verticale o orizzontale e il colore di sfondo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void
InitialiseDisplay() {
display.init(
0
);
SPI.end();
SPI.begin(EPD_SCK, EPD_MISO, EPD_MOSI, EPD_CS);
u8g2Fonts.begin(display);
u8g2Fonts.setFontMode(
1
);
u8g2Fonts.setFontDirection(
0
);
u8g2Fonts.setForegroundColor(GxEPD_BLACK);
u8g2Fonts.setBackgroundColor(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB10_tf);
display.fillScreen(GxEPD_WHITE);
display.setFullWindow();
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
105
,
90
,
"Please wait."
, LEFT);
drawString(
65
,
180
,
"Retrieving Data..."
, LEFT);
display.display(
false
);
}
Quindi inizializziamo l’IC di monitoraggio della batteria,lo impostiamo sulla capacità della batteria di 3000 mAh (se si utilizza tale batteria) per avere una stima migliore della durata e della durata della batteria. Dopodiché, se la batteria è troppo scarica, interrompiamo tutte le operazioni andando subito in sleepmode e dando un avviso sul display con la scritta “WARNING! RACHARGE BATTERY”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if
(
!
gg.begin()) {
while
(
1
)
delay
(
10
);
}
gg.setCellCapacity(LC709203F_APA_3000MAH);
gg.setCellProfile( LC709203_NOM3p7_Charge4p2 ) ;
voltage
=
gg.cellVoltage_mV()
/
1000.0
;
if
(voltage <
=
3.45
)
{
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
100
,
75
,
"WARNING!"
, LEFT);
drawString(
30
,
175
,
"RECHARGE BATTERY"
, LEFT);
display.display(
false
);
display.powerOff();
esp_sleep_enable_ext0_wakeup(GPIO_NUM_15,
1
);
esp_deep_sleep_start();
}
Se non vengono rilevati problemi con la batteria avviamo la connessione WiFi
con la libreria WiFi Manager:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
WiFi.mode(WIFI_STA);
wifiManager.setConfigPortalTimeout(
300
);
WiFi.begin();
timeout
=
millis
();
while
(WiFi.status()
!
=
WL_CONNECTED) {
delay
(
250
);
if
(
millis
()
-
timeout >
25000
)
{
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
85
,
110
,
"Please Set WiFi"
, LEFT);
u8g2Fonts.setFont(u8g2_font_helvB18_tf);
drawString(
35
,
180
,
"LineaMeteoStazioneVisual"
, LEFT);
display.display(
false
);
res
=
wifiManager.autoConnect(
"LineaMeteoStazioneVisual"
,
"LaMeteo2005"
);
if
(
!
res) {
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
85
,
110
,
"Failed to connect"
, LEFT);
u8g2Fonts.setFont(u8g2_font_helvB18_tf);
drawString(
35
,
180
,
"Reset or wait 1 minute"
, LEFT);
esp_sleep_enable_timer_wakeup(
60
*
1000000LL);
esp_deep_sleep_start();
}
}
}
Quando viene stabilita la connessione, ci colleghiamo a Firebase con le nostre credenziali di Firebase come spiegato nell’articolo che parla della stazione metereologica.
1
2
3
4
Firebase.begin(FIREBASE_HOST, FIREBASE_AUTH);
Firebase.reconnectWiFi(
true
);
Firebase.setMaxRetry(Weather,
2
);
Quindi controlliamo se è necessario un reset per il WiFi, controllando Firebase per qualsiasi input e se è uguale a 1, farà un reset delle impostazioni WiFi e proverà a riconnettersi nuovamente e entrerà in modalità AP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
if
(Firebase.getInt(Weather,
"/Connection/DEVICE3/ResetWiFi"
))
{
WiFiReset
=
Weather.to<
int
>();
}
if
(WiFiReset
=
=
1
)
{
WiFiReset
=
0
;
Firebase.setInt(Weather,
"/Connection/DEVICE3/ResetWiFi"
,
0
);
wifiManager.resetSettings();
WiFi.mode(WIFI_STA);
wifiManager.setConfigPortalTimeout(
300
);
WiFi.begin();
timeout
=
millis
();
while
(WiFi.status()
!
=
WL_CONNECTED) {
delay
(
250
);
if
(
millis
()
-
timeout >
10000
)
{
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
85
,
110
,
"Please Set WiFi"
, LEFT);
u8g2Fonts.setFont(u8g2_font_helvB18_tf);
drawString(
35
,
180
,
"LineaMeteoStazioneVisual"
, LEFT);
display.display(
false
);
res
=
wifiManager.autoConnect(
"LineaMeteoStazioneVisual"
,
"LaMeteo2005"
);
if
(
!
res) {
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
85
,
110
,
"Failed to connect"
, LEFT);
u8g2Fonts.setFont(u8g2_font_helvB18_tf);
drawString(
35
,
180
,
"Reset or wait 1 minute"
, LEFT);
esp_sleep_enable_timer_wakeup(
60
*
1000000LL);
esp_deep_sleep_start();
}
}
}
InitialiseDisplay();
}
Quindi controlliamo se sono necessari alcuni aggiornamenti per il nostro display. (Quando “enable” è scritto in “update” in Firebase, consultare la guida generale per maggiori informazioni)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
if
(Firebase.getString(Weather,
"/Connection/DEVICE3/Update"
))
{
DEVICE3OTA
=
Weather.to<
const
char
*
>();
}
if
(DEVICE3OTA
=
=
"enable"
)
{
Firebase.setString(Weather,
"/Connection/DEVICE3/Update"
,
"disable"
);
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB18_tf);
drawString(
75
,
110
,
"Checking for Updates..."
, LEFT);
display.display(
false
);
delay
(
4000
);
update();
u8g2Fonts.setFont(u8g2_font_helvB18_tf);
drawString(
75
,
180
,
"No Remote Updates"
, LEFT);
display.display(
false
);
delay
(
2000
);
}
if
(DEVICE3OTA
=
=
"enable"
)
{
MDNS.begin(host);
if
(MDNS.begin(
"esp32"
)) {
Serial.println
(
"mDNS responder started"
);
}
httpUpdater.
setup
(
&
httpServer);
httpServer.begin();
MDNS.addService(
"http"
,
"tcp"
,
80
);
Firebase.setString(Weather,
"/Connection/DEVICE3/Update"
,
"disable"
);
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
25
,
110
,
"Update Local Firmware"
, LEFT);
u8g2Fonts.setFont(u8g2_font_helvB18_tf);
drawString(
85
,
180
,
"Go to "
+
WiFi.localIP().toString()
+
"/update"
, LEFT);
u8g2Fonts.setFont(u8g2_font_helvB14_tf);
drawString(
85
,
250
,
"Press Button to Cancel"
, LEFT);
display.display(
false
);
delay
(
50
);
Firebase.setString(Weather,
"/Connection/DEVICE3/UpdateHere"
, String(WiFi.localIP().toString()
+
"/update"
));
delay
(
50
);
TIMEROTA
=
millis
();
}
Quindi avviamo la connessione NTP per l’ora con timeClient.begin(); e poi otteniamo alcuni parametri utili come il fuso orario e le unità di misura dei dati meteorologici visualizzati.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void
getDataTime()
{
if
(Firebase.getInt(Weather,
"/Time/TIMEZONE"
))
{
TIMEZONE
=
Weather.to<
int
>();
}
else
{
}
if
(Firebase.getString(Weather,
"/Services/OpenWeather/API"
))
{
api_key
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Display/Language"
))
{
language
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Services/OpenWeather/Latitude"
))
{
latitude
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Services/OpenWeather/Longitude"
))
{
longitude
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Services/OpenWeather/Hemisphere"
))
{
Hemisphere
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Display/Units/Temperature"
))
{
unitsTemperature
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Display/Units/Wind"
))
{
unitsWind
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Display/Units/Rain"
))
{
unitsRain
=
Weather.to<
const
char
*
>();
}
if
(Firebase.getString(Weather,
"/Display/Units/Pressure"
))
{
unitsPressure
=
Weather.to<
const
char
*
>();
}
}
E alla fine del setup, iniziamo la comunicazione con il BME680 e impostiamo il pin 15 in input per il nostro pulsante.
1
2
3
4
5
6
7
8
Wire.begin();
bme.begin();
bme.setTemperatureOversampling(BME680_OS_8X);
bme.setHumidityOversampling(BME680_OS_2X);
bme.setIIRFilterSize(BME680_FILTER_SIZE_3);
bme.setGasHeater(
320
,
150
);
pinMode
(
15
,
INPUT
);
Codice di Loop
Entriamo quindi nel loop e se è stato rilevato un aggiornamento in modalità locale nel setup entriamo in un loop che gestisce il server dove possiamo caricare il file per l’aggiornamento del firmware. Rimarrà in questa modalità per 5 minuti, dopodiché si riavvierà se non viene rilevato nulla.
Se nessun aggiornamento OTA è abilitato, inizierà in normale operazione. Otteniamo prima i dati meteo dal database e li visualizziamo sul display.
getValues(); e DisplayWeather(); sono le funzioni create nel codice che gestiscono questo. In sintesi, getValues(); controlla eventuali impostazioni da applicare sul display, come le unità di temperatura, pressione, vento e pioggia. Riceve anche tutti i valori attuali, massimi e minimi dalla stazione metereologica esterna, inoltre legge il sensore BME680 e calcola la qualità dell’aria. DisplayWeather(); poi utilizza la libreria grafica per visualizzare i dati sul display.
Dopo che i dati sono stati scritti sullo schermo, usiamo display.display(false); display.powerOff(); per aggiornare completamente il display e spegnere il display per un migliore consumo energetico. Se si utilizza la modalità Fast refresh, invece di attivare la modalità sleep, torniamo invece in lettura e scrittura dei dati sul display ogni 50 secondi. Leggiamo anche se il pulsante è stato premuto con:
1
2
3
4
5
if
(
digitalRead
(
15
)
=
=
HIGH
)
{
Firebase.setString(Weather,
"/Connection/DEVICE3/Update"
,
"enable"
);
ESP.restart();
}
In caso affermativo, riavviamo l’ESP e verifichiamo la presenza di un aggiornamento. Se la batteria è scarica, interrompiamo comunque ogni operazione e entriamo in modalità di sospensione con il segnale di avviso sul display come spiegato in precedenza.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void
loop
() {
if
(DEVICE3OTA
=
=
"enable"
)
{
httpServer.handleClient();
if
(
digitalRead
(
15
)
=
=
HIGH
)
{
ESP.restart();
}
if
(
millis
()
-
TIMEROTA >
300000
)
{
ESP.restart();
}
}
else
{
if
(
digitalRead
(
15
)
=
=
HIGH
)
{
Firebase.setString(Weather,
"/Connection/DEVICE3/Update"
,
"enable"
);
ESP.restart();
}
if
(
millis
()
-
refreshcount >
=
refreshtime)
{
refreshtime
=
50000
;
refreshcount
=
millis
();
percentage
=
gg.cellRemainingPercent10()
/
10
;
voltage
=
gg.cellVoltage_mV()
/
1000.0
;
getValues();
DisplayWeather();
display.display(
false
);
display.powerOff();
if
(FASTREFRESH
=
=
"NO"
)
{
BeginSleep();
}
if
(voltage <
=
3.45
)
{
display.fillScreen(GxEPD_WHITE);
u8g2Fonts.setFont(u8g2_font_helvB24_tf);
drawString(
100
,
75
,
"WARNING!"
, LEFT);
drawString(
30
,
175
,
"RECHARGE BATTERY"
, LEFT);
display.powerOff();
esp_sleep_enable_ext0_wakeup(GPIO_NUM_15,
1
);
esp_deep_sleep_start();
}
}
}
}
Links
LineaMeteoStazione: Guida Tecnica Dispositivo Master, Invio e Raccolta Dati LineaMeteoStazione: Guida Tecnica Display LineaMeteoStazione: Guida Tecnica Ricevitore, rete e dispositivo di gestione LineaMeteoStazione: La Stazione Meteo Personalizzabile con ESP32, ESP8266, Attiny85 e aggiornamenti OTA
Per la stazione metereologica pre-assemblata o l’origine dei materiale per favore scrivimi Eugenioeugenioiaquinta@outlook.it
LINEAMETEO TOPIC