sâmbătă, 4 aprilie 2026

VU metru cu Arduino si ST7735

 Va salut !

La cererea prietenului Dragos (care mi-a trimis un clip cu un vu-metru cu display color), bazandu-ma pe IA am conceput acest vu-metru, cu un Arduino Nano si un display LCD tip ST7735. 

Are un encoder care permite selectia diferitelor tipuri de afisare ; rotirea encoderului permite navigarea printre moduri, apasarea permite selectarea/deselectarea modurilor (se pot selecta toate). Prin combinatiile intre moduri se poate ajunge la o functionare conforma preferintelor.

Semnalul audio se aplica pe pinii A0 si A1.

Aici am postat un mic clip cu functionarea.

Enjoy ! 


 

UPDATE 12.07.2026 : Soft modificat ; functioneaza bine cu preamp de microfon condenser cu LM386

#include <Adafruit_GFX.h>    
#include <Adafruit_ST7735.h>
#include <SPI.h>
#include <Encoder.h>

#define TFT_CS 10
#define TFT_RST 9
#define TFT_DC 8
Adafruit_ST7735 tft = Adafruit_ST7735(TFT_CS, TFT_DC, TFT_RST);

Encoder myEnc(2, 3);
#define ENC_SW 4

const char* menuOptions[] = {"PEAK HLD", "FULL SCR", "WIDE AGC", "SMOOTH"};
const uint16_t menuColors[] = {ST7735_WHITE, ST7735_YELLOW, 0x07E0, 0x07FF};

bool optStates[] = {true, false, false, false};
int menuIndex = 0;
int lastMenuIndex = -1;
long oldPosition = -999;
int leftPeak = 0, rightPeak = 0;
int lastLeftPeakY = 139, lastRightPeakY = 139;
unsigned long lastPeakMillis = 0;
unsigned long lastButtonMillis = 0;

byte lastStateL[15];
byte lastStateR[15];
int lastLvlH = -1;

const int BORDER_X = 65;
const int BORDER_W = 58;
const int BAR_W = 18;
const int PADDING = 6;
const int POS_L = BORDER_X + PADDING;
const int POS_R = BORDER_X + BORDER_W - PADDING - BAR_W;

void setup() {
pinMode(ENC_SW, INPUT_PULLUP);
tft.initR(INITR_BLACKTAB);
tft.setRotation(0);
tft.fillScreen(ST7735_BLACK);

for(int i = 0; i < 15; i++) {
lastStateL[i] = 255;
lastStateR[i] = 255;
}

deseneazaInterfataStatica();
actualizeazaSelector(0);
}

void loop() {
// 1. ROTATIE ENCODER
long newPos = myEnc.read() / 4;
if (newPos != oldPosition) {
menuIndex = abs(newPos % 4);
actualizeazaSelector(menuIndex);
oldPosition = newPos;
}

// 2. APASARE BUTON
if (digitalRead(ENC_SW) == LOW) {
if (millis() - lastButtonMillis > 300) {
optStates[menuIndex] = !optStates[menuIndex];
tft.fillRect(59, 40 + (menuIndex * 12), 4, 8, ST7735_BLACK);
if (optStates[menuIndex]) {
tft.fillRect(59, 43 + (menuIndex * 12), 3, 3, ST7735_GREEN);
}
lastButtonMillis = millis();
}
}

// --- 3. ESANTIONARE AUDIO STEREO REALA ---
int signalMaxL = 0, signalMinL = 1023;
int signalMaxR = 0, signalMinR = 1023;

unsigned long startMillis = millis();
while (millis() - startMillis < 30) {
int sampleL = analogRead(A0);
int sampleR = analogRead(A1);

if (sampleL < 1024) {
if (sampleL > signalMaxL) signalMaxL = sampleL;
if (sampleL < signalMinL) signalMinL = sampleL;
}
if (sampleR < 1024) {
if (sampleR > signalMaxR) signalMaxR = sampleR;
if (sampleR < signalMinR) signalMinR = sampleR;
}
}

// Calculam amplitudinea Peak-to-Peak (Stereo Independent)
int peakToPeakL = signalMaxL - signalMinL;
int peakToPeakR = signalMaxR - signalMinR;


// Sensibilitatea dinamica din meniu (WIDE AGC)
int maxVal = optStates[2] ? 40 : 180;

int segL = constrain(map(peakToPeakL, 4, maxVal, 0, 15), 0, 15);
int segR = constrain(map(peakToPeakR, 4, maxVal, 0, 15), 0, 15);

// 4. LOGICA PEAK HOLD
int curPeakL = map(segL, 0, 15, 0, 92);
int curPeakR = map(segR, 0, 15, 0, 92);

if (curPeakL > leftPeak) leftPeak = curPeakL;
if (curPeakR > rightPeak) rightPeak = curPeakR;

// --- TIMING SI VITEZA ACCELERATA DE REVENIRE LA ZERO ---
int fallSpeed = optStates[3] ? 90 : 35; // Mai rapid decat inainte (35ms vs 50ms)
if (millis() - lastPeakMillis > fallSpeed) {
if (leftPeak > 0) leftPeak -= 3; // REPARAT: Scade cu 3 pixeli deodata pentru o coborare mult mai alerta
if (rightPeak > 0) rightPeak -= 3; // REPARAT: Scade cu 3 pixeli deodata pentru o coborare mult mai alerta

if (leftPeak < 0) leftPeak = 0;
if (rightPeak < 0) rightPeak = 0;
lastPeakMillis = millis();
}

if (!optStates[0]) { leftPeak = 0; rightPeak = 0; }

// 5. DESENARE STEREO INDEPENDENTA
actualizeazaBaraBruta(POS_L, segL, leftPeak, lastLeftPeakY, optStates[0], lastStateL);
actualizeazaBaraBruta(POS_R, segR, rightPeak, lastRightPeakY, optStates[0], lastStateR);

// 6. INDICATOR LEVEL
int lvlH = constrain(map(peakToPeakL, 4, maxVal, 0, 49), 0, 49);
if (lvlH != lastLvlH) {
int yBaseLevel = 140;
tft.fillRect(30, yBaseLevel - lvlH, 10, lvlH, 0x5DFF);
if (lvlH < 49) {
tft.fillRect(30, 91, 10, 49 - lvlH, 0x000F);
}
lastLvlH = lvlH;
}
}

void actualizeazaBaraBruta(int x, int val, int peak, int &lY, bool peakActive, byte last[]) {
for (int i = 0; i < 15; i++) {
byte s = (i < val) ? 1 : 0;
if (s != last[i]) {
uint16_t c = (s == 1) ? (i < 9 ? ST7735_GREEN : (i < 12 ? ST7735_YELLOW : ST7735_RED)) : 0x1082;
tft.fillRect(x, 135 - (i * 6), BAR_W, 4, c);
last[i] = s;
}
}
int cY = 139 - peak;
if (cY != lY) {
tft.drawFastHLine(x, lY, BAR_W, ST7735_BLACK);
lY = cY;
}
if (peakActive && peak > 2) {
tft.drawFastHLine(x, cY, BAR_W, ST7735_WHITE);
}
}

void deseneazaInterfataStatica() {
tft.fillRect(5, 5, 118, 20, ST7735_BLUE);
tft.setTextColor(ST7735_WHITE);
tft.setCursor(38, 12); tft.print("VU METER");
for (int i = 0; i < 4; i++) {
tft.setTextColor(menuColors[i]);
tft.setCursor(10, 40 + (i * 12)); tft.print(menuOptions[i]);
}
tft.drawRect(BORDER_X, 40, BORDER_W, 101, ST7735_WHITE);
tft.fillRect(POS_L, 145, BAR_W, 12, ST7735_RED);
tft.fillRect(POS_R, 145, BAR_W, 12, ST7735_RED);
tft.setTextColor(ST7735_WHITE);
tft.setCursor(POS_L + 6, 147); tft.print("L");
tft.setCursor(POS_R + 6, 147); tft.print("R");
int xLvl = 30; int yTop = 90; int yBot = 141;
tft.drawRect(xLvl - 1, yTop, 12, (yBot - yTop) + 1, ST7735_WHITE);
tft.setCursor(12, yTop + 2); tft.print("10");
tft.setCursor(18, yBot - 8); tft.print("0");
tft.setCursor(20, 147); tft.print("LEVEL");
for (int i = 0; i <= 10; i++) {
int yT = yBot - (i * 5);
tft.drawFastHLine(xLvl - 4, yT, 3, ST7735_WHITE);
tft.drawFastHLine(xLvl + 11, yT, 3, ST7735_WHITE);
}
}

void actualizeazaSelector(int index) {
if (index != lastMenuIndex) {
if (lastMenuIndex >= 0) tft.fillRect(1, 40 + (lastMenuIndex * 12), 6, 8, ST7735_BLACK);
tft.setTextColor(ST7735_RED); tft.setCursor(1, 40 + (index * 12)); tft.print(">");
lastMenuIndex = index;
}
for(int i = 0; i < 4; i++) {
tft.fillRect(59, 40 + (i * 12), 4, 8, ST7735_BLACK);
if (optStates[i]) tft.fillRect(59, 43 + (i * 12), 3, 3, ST7735_GREEN);
}
}

joi, 19 martie 2026

Ceas analogic - OLED GC9A01 si Wemos D1 mini

Acesta este proiectul unui ceas analogic. Se poate configura intr-o multitudine de "fețe". In poze sunt preferatele mele. Enjoy !

 

 


#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <TFT_eSPI.h>
#include <TimeLib.h>

// --- CONFIGURARE WIFI ---
const char *ssid = "yourSSID";
const char *password = "yourPSW";

TFT_eSPI tft = TFT_eSPI();
TFT_eSprite spr = TFT_eSprite(&tft);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", 0, 86400000); // Sincronizare la 24h

uint16_t palettePtr[16];

// --- PROTOTIPURI ---
void drawStickHand(float deg, int len, int tail, int width, int colIdx);
int getOffset(time_t t);

void setup() {
tft.init();
tft.setRotation(0);
tft.fillScreen(TFT_BLACK);

tft.setTextColor(TFT_WHITE);
tft.setTextDatum(MC_DATUM);
tft.setFreeFont(&FreeSansBold12pt7b);
tft.drawString("Conectare...", 120, 120);

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) delay(500);

timeClient.begin();
timeClient.update();

// --- PALETĂ (4 biți = 16 culori) ---
palettePtr[0] = TFT_BLACK;
palettePtr[1] = tft.color565(230, 0, 0); // Roșu intens ace
palettePtr[2] = TFT_WHITE; // Cifre / Gradații
palettePtr[3] = tft.color565(80, 80, 80); // Gradații minute (Gri)
palettePtr[4] = tft.color565(140, 140, 140); // Dată (Gri discret)
palettePtr[5] = tft.color565(0, 255, 120); // Verde Neon pentru secundar

spr.setColorDepth(4);
spr.createSprite(240, 240);
spr.createPalette(palettePtr, 16);
}

void loop() {
timeClient.update();
time_t rawTime = timeClient.getEpochTime();
time_t localTime = rawTime + getOffset(rawTime);

int h = hour(localTime) % 12, m = minute(localTime), s = second(localTime);
float sDeg = s * 6, mDeg = m * 6 + s * 0.1, hDeg = h * 30 + m * 0.5;

spr.fillScreen(0);

// 1. GRADAȚII MARGINE
for (int i = 0; i < 60; i++) {
float rad = (i * 6 - 90) * 0.0174532925;
float w = (i % 5 == 0) ? 0.035 : 0.018;
int rIn = (i % 5 == 0) ? 105 : 112;
int x1 = 120 + cos(rad - w) * rIn; int y1 = 120 + sin(rad - w) * rIn;
int x2 = 120 + cos(rad + w) * rIn; int y2 = 120 + sin(rad + w) * rIn;
int x3 = 120 + cos(rad - w) * 120; int y3 = 120 + sin(rad - w) * 120;
int x4 = 120 + cos(rad + w) * 120; int y4 = 120 + sin(rad + w) * 120;
spr.fillTriangle(x1, y1, x2, y2, x3, y3, (i % 5 == 0) ? 2 : 3);
spr.fillTriangle(x2, y2, x3, y3, x4, y4, (i % 5 == 0) ? 2 : 3);
}

// 2. CIFRE MARI
spr.setFreeFont(&FreeSansBold18pt7b); spr.setTextColor(2); spr.setTextDatum(MC_DATUM);
spr.drawString("12", 120, 42); spr.drawString("06", 120, 198);
spr.drawString("09", 42, 120); spr.drawString("03", 198, 120);

// 3. DATA (zz-ll-aaaa)
spr.setFreeFont(&FreeSans9pt7b); spr.setTextColor(4);
char dataStr[12]; sprintf(dataStr, "%02d-%02d-%04d", day(localTime), month(localTime), year(localTime));
spr.drawString(dataStr, 120, 155);

// 4. ACE ORE & MINUTE
// Parametri: Unghi, Lungime vârf, Lungime, Lățime, Culoare
drawStickHand(hDeg, 58, 18, 10, 1); // Ore: Grosime 10px, coadă 18px
drawStickHand(mDeg, 105, 18, 6, 1); // Minute: Lung (până la marcaje), subțire 6px, coadă 18px

// 5. SECUNDARUL
drawStickHand(sDeg, 110, 25, 2, 5); // Secundar: Foarte subțire 2px, coadă lungă 25px

// 6. CENTRUL (Cerc alb cu contur negru)
spr.fillCircle(120, 120, 4, 2);
spr.drawCircle(120, 120, 4, 0);

spr.pushSprite(0, 0);
delay(150);
}

// --- FUNCȚIE PENTRU ACE DREPTUNGHIULARE ---
void drawStickHand(float deg, int len, int tail, int width, int colIdx) {
float rad = (deg - 90) * 0.0174532925;
// Vector normal pentru lățime constantă
float nx = -sin(rad) * (width / 2.0);
float ny = cos(rad) * (width / 2.0);

// Calculăm cele 4 puncte: coada acului se extinde în direcția opusă vârfului
int x1 = 120 - cos(rad) * tail + nx; int y1 = 120 - sin(rad) * tail + ny;
int x2 = 120 - cos(rad) * tail - nx; int y2 = 120 - sin(rad) * tail - ny;
int x3 = 120 + cos(rad) * len + nx; int y3 = 120 + sin(rad) * len + ny;
int x4 = 120 + cos(rad) * len - nx; int y4 = 120 + sin(rad) * len - ny;

spr.fillTriangle(x1, y1, x2, y2, x3, y3, colIdx);
spr.fillTriangle(x2, y2, x3, y3, x4, y4, colIdx);
}

int getOffset(time_t t) {
tmElements_t tm; breakTime(t, tm);
if (tm.Month < 3 || tm.Month > 10) return 7200;
if (tm.Month > 3 && tm.Month < 10) return 10800;
int dSun = tm.Day - ((tm.Wday - 1 + 7) % 7);
if (tm.Month == 3) return (dSun >= 25) ? 10800 : 7200;
return (dSun >= 25) ? 7200 : 10800;
}

duminică, 1 martie 2026

O simpla statie de lipit cu Arduino

 

Va salut !

Va prezint statia mea de lipit cu Arduino. Realizata cu AI Gemini, updatata cu Vercel si Qwen ; eu doar am formulat cerintele si am testat codul, deci nu am prea multe merite. Inspiratia mea au fost nenumaratele exemple existente.

Pe scurt : ciocan Pensol SL-10 (cel pe care-l am de mai bine de 15 ani si nu renunt la el, e prea bun !), Arduino Nano, display LED 7 segmente 3 digiti, encoder rotativ, 2 butoane pentru memorare 2 temperaturi de lucru, stand-by dupa 30 minute.

Am vrut initial cu PID, dar inertia termica mare a ciocanului m-a facut sa renunt ; Qwen a sugerat un algoritm "predictiv" surprinzator de eficient .

Orice comentariu sau sugestie sunt binevenite. Statia este prezentata si pe Elforum (https://www.elforum.info/topic/165506-statie-de-lipit-cu-arduino/), cine vrea o poate modifica dupa cum doreste (recomand Vercel / Perplexity / Qwen / Gemini, nu neaparat in aceasta ordine). Numai bine ! 

 

Update : corectate buguri. Initial am crezut ca nu e mare lucru sa faci o astfel de statie...Realitatea insa mi-a demonstrat contrariul - sunt n factori care o pot face sa mearga prost. In timp cred ca am testat mai mult de 20 de versiuni ; fiecare avea niste neajunsuri. Varianta postata pare sa mearga bine. Doar testarea intensiva imi va confirma asta.



 

 

 


/*
//
// STATIE DE LIPIT CU ARDUINO
// DISPLAY LED CU 7 SEGMENTE
// ENCODER SI 2 MEMORII
// STAND-BY DUPA 30 MINUTE
// pt PENSOL SL-10
//
// revizia N - august 2026
// - 100 nF ceramic intre T+ si T-, fire termocuplu rasucite;
// - IRLZ44N low-side, active-high, pull-down 10 k pe poarta.
//
// www.elforum.info/topic/165506-statie-de-lipit-cu-arduino
*/


#include <Encoder.h>
#include <EEPROM.h>
#include <EasyButton.h>
#include <avr/wdt.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <math.h>

#define CALIBRATE_HOLD_BASE 0 // 1 doar temporar, pentru calibrare
#define HEATER_ACTIVE_LOW 0 // active-high
#define HEATER_QUIET_DURING_READ 1 // heater oprit ~1 ms pe durata citirii

// --- CONFIGURARE HARDWARE ---
const int TC_SCK = 10;
const int TC_CS = 9;
const int TC_SO = 8;

Encoder myEnc(2, 3);

const int segPins[] = {4, 5, 6, 7, 12, 13, A0};
const int digitPins[] = {A3, A4, A5};
const int heaterPin = 11;

// --- CONSTANTE DE SISTEM ---
const unsigned long SAMPLE_INTERVAL_MS = 250UL;
const unsigned long TIMEOUT_OFF = 1800000UL; // 30 minute
const unsigned long BLINK_TIME_MS = 500UL;

const float SETPOINT_MIN = 150.0f;
const float SETPOINT_MAX = 400.0f;

const float MIN_VALID_TEMP = 10.0f;
const float MAX_VALID_TEMP = 480.0f;
const float MAX_SAFE_TEMP = 450.0f;

const float MAX_HEAT_RATE = 15.0f;
const float MAX_COOL_RATE = 5.0f;
const float RATE_MARGIN = 3.0f;
const uint8_t OUTLIER_ACCEPT_STREAK = 3;
const uint8_t INVALID_ERR_COUNT = 8; // ~2 s citiri invalide => E1

const float OVERHEAT_HYST = 20.0f;
const unsigned long OVERHEAT_MIN_OFF_MS = 10000UL;

const unsigned long HEAT_FAULT_MS = 30000UL;
const float HEAT_FAULT_GAP = 60.0f;
const float HEAT_FAULT_RISE = 5.0f;
const unsigned long FAULT_RETRY_MS = 10000UL;

const float CJ_OFFSET = 0.0f;

const float DEADBAND = 1.5f;
const float HOLD_TREND_GAIN = 50.0f;
const float HOLD_ERROR_GAIN = 20.0f;

// --- VARIABILE CONTROL ---
float Setpoint = 380.0f;
float Input = 0.0f;
int pwm = 0;
float lastInput = 0.0f;
float tempTrend = 0.0f;

int pwm_hold_base = 185;

bool inputInitialized = false;
bool needFilterReset = true;
bool sensorError = false;
uint8_t errSource = 0; // 1=E1, 2=E2, 3=E3
uint8_t invalidStreak = 0;
uint8_t outlierStreak = 0;

bool overheatLatch = false;
unsigned long overheatSince = 0;

bool heatFault = false;
unsigned long faultSince = 0;
unsigned long highPwmSince = 0;
float tempAtHighPwm = 0.0f;

long oldPosition = 0;
unsigned long lastUpdate = 0;
unsigned long lastEncoderTime = 0;
unsigned long lastAdjustTime = 0;

// --- MEMORIE, TIMEOUT, BLINK ---
const int EEPROM_ADDR[] = {0, 4};
int savedTemps[] = {0, 0};
volatile unsigned long blinkStart = 0;
unsigned long lastActivityTime = 0;
bool isSystemOff = false;

bool pendingSave = false;
uint8_t pendingSaveSlot = 0;

EasyButton btn1(A1);
EasyButton btn2(A2);

// --- VARIABILE PENTRU DISPLAY ---
volatile uint8_t isrCurrentDigit = 2;
volatile int isrDisplayValue = 0;
volatile bool isrDisplayEnabled = true;

const byte digits[] = {
B00111111, B00000110, B01011011, B01001111, B01100110,
B01101101, B01111101, B00000111, B01111111, B01101111
};

inline void setHeaterPWM(int value) {
if (value < 0) value = 0;
if (value > 255) value = 255;
#if HEATER_ACTIVE_LOW
analogWrite(heaterPin, 255 - value);
#else
analogWrite(heaterPin, value);
#endif
}

// --- DRIVER MAX6675 PROPRIU ---
void tcInit() {
pinMode(TC_SCK, OUTPUT);
digitalWrite(TC_SCK, LOW);
pinMode(TC_CS, OUTPUT);
digitalWrite(TC_CS, HIGH);
pinMode(TC_SO, INPUT);
}

uint16_t tcReadRaw() {
uint16_t v = 0;
digitalWrite(TC_SCK, LOW);
delayMicroseconds(10);
digitalWrite(TC_CS, LOW);
delayMicroseconds(1);
for (int8_t i = 15; i >= 0; i--) {
digitalWrite(TC_SCK, HIGH);
delayMicroseconds(1);
digitalWrite(TC_SCK, LOW);
delayMicroseconds(1);
v <<= 1;
if (digitalRead(TC_SO)) v |= 1;
}
digitalWrite(TC_CS, HIGH);
return v;
}

/*
Trei transferuri SPI cu intreruperile OPRITE + mediana.
~0.7-1 ms total: afisajul sare maxim un tick de 1 ms (invizibil),
millis() pierde <0.5% (neglijabil).
Returneaza NAN la fault de termocuplu sau cadru imposibil.
*/
float tcReadCelsius() {
noInterrupts();
uint16_t a = tcReadRaw();
uint16_t b = tcReadRaw();
uint16_t c = tcReadRaw();
interrupts();

uint16_t t;
if (a > b) { t = a; a = b; b = t; }
if (b > c) { t = b; b = c; c = t; }
if (a > b) { t = a; a = b; b = t; }
uint16_t v = b;

if (v & 0x8000) return NAN; // bit de semn = 1 => cadru corupt
if (v & 0x0004) return NAN; // bit 2 = termocuplu intrerupt
return (float)((v >> 3) & 0x0FFF) * 0.25f;
}

// --- FUNCTII AUXILIARE ---
void triggerBlink() {
blinkStart = millis();
if (blinkStart == 0) blinkStart = 1;
}

void resetActivity() {
lastActivityTime = millis();
}

void wakeSystem() {
if (isSystemOff) {
isSystemOff = false;
needFilterReset = true;
}
heatFault = false;
highPwmSince = 0;
resetActivity();
lastAdjustTime = millis();
}

void saveTempSlot(uint8_t slot) {
if (slot > 1) return;
int newValue = (int)Setpoint;
if (newValue < (int)SETPOINT_MIN || newValue > (int)SETPOINT_MAX) return;
if (savedTemps[slot] != newValue) {
savedTemps[slot] = newValue;
EEPROM.put(EEPROM_ADDR[slot], savedTemps[slot]);
}
lastAdjustTime = millis();
triggerBlink();
}

void onBtn1Short() {
wakeSystem();
if (savedTemps[0] > 0) Setpoint = (float)savedTemps[0];
triggerBlink();
}
void onBtn1Long() {
wakeSystem();
pendingSaveSlot = 0;
pendingSave = true;
}
void onBtn2Short() {
wakeSystem();
if (savedTemps[1] > 0) Setpoint = (float)savedTemps[1];
triggerBlink();
}
void onBtn2Long() {
wakeSystem();
pendingSaveSlot = 1;
pendingSave = true;
}

void filterUpdate(float sample, float dt) {
float newInput = (Input * 0.75f) + (sample * 0.25f);
float up = lastInput + (MAX_HEAT_RATE * dt + RATE_MARGIN);
float dn = lastInput - (MAX_COOL_RATE * dt + RATE_MARGIN);
if (newInput > up) newInput = up;
if (newInput < dn) newInput = dn;

float instantTrend = (newInput - lastInput) / dt;
if (instantTrend > MAX_HEAT_RATE) instantTrend = MAX_HEAT_RATE;
if (instantTrend < -MAX_COOL_RATE) instantTrend = -MAX_COOL_RATE;
tempTrend = (tempTrend * 0.6f) + (instantTrend * 0.4f);

Input = newInput;
lastInput = newInput;
}

// --- ISR DISPLAY ---
ISR(TIMER1_COMPA_vect) {
digitalWrite(digitPins[isrCurrentDigit], LOW);
isrCurrentDigit = (isrCurrentDigit + 1) % 3;

byte segments = 0;
if (!isrDisplayEnabled) {
segments = 0;
} else if (isrDisplayValue == -1) {
// STb
if (isrCurrentDigit == 0) segments = B01101101;
else if (isrCurrentDigit == 1) segments = B01111000;
else segments = B01111100;
} else if (isrDisplayValue <= -111 && isrDisplayValue >= -113) {
// E1 / E2 / E3
if (isrCurrentDigit == 0) segments = B01111001; // E
else if (isrCurrentDigit == 1) segments = digits[-110 - isrDisplayValue]; // 1..3
else segments = 0;
} else {
int t = isrDisplayValue;
if (t < 0) t = -t;
int d;
if (isrCurrentDigit == 0) d = (t / 100) % 10;
else if (isrCurrentDigit == 1) d = (t / 10) % 10;
else d = t % 10;
if (isrCurrentDigit == 0 && d == 0 && t < 100) segments = 0;
else if (isrCurrentDigit == 1 && d == 0 && t < 10) segments = 0;
else segments = digits[d];
}
for (int i = 0; i < 7; i++) {
digitalWrite(segPins[i], (segments & (1 << i)) ? LOW : HIGH);
}
digitalWrite(digitPins[isrCurrentDigit], HIGH);
}

void setupTimer1() {
cli();
TCCR1A = 0;
TCCR1B = 0;
TCNT1 = 0;
OCR1A = 249; // 16 MHz / 64 / 250 = 1000 Hz
TCCR1B |= (1 << WGM12);
TCCR1B |= (1 << CS11) | (1 << CS10);
TIFR1 |= (1 << OCF1A);
TIMSK1 |= (1 << OCIE1A);
sei();
}

void setup() {
MCUSR = 0;
wdt_disable();

#if HEATER_ACTIVE_LOW
digitalWrite(heaterPin, HIGH);
#else
digitalWrite(heaterPin, LOW);
#endif
pinMode(heaterPin, OUTPUT);
setHeaterPWM(0);

for (int i = 0; i < 7; i++) {
pinMode(segPins[i], OUTPUT);
digitalWrite(segPins[i], HIGH);
}
for (int i = 0; i < 3; i++) {
pinMode(digitPins[i], OUTPUT);
digitalWrite(digitPins[i], LOW);
}

pinMode(A1, INPUT_PULLUP);
pinMode(A2, INPUT_PULLUP);

tcInit();

isrDisplayValue = (int)Setpoint;
isrDisplayEnabled = true;
setupTimer1();

btn1.begin();
btn2.begin();
btn1.onPressed(onBtn1Short);
btn1.onPressedFor(2000, onBtn1Long);
btn2.onPressed(onBtn2Short);
btn2.onPressedFor(2000, onBtn2Long);

for (uint8_t i = 0; i < 2; i++) {
int val = 0;
EEPROM.get(EEPROM_ADDR[i], val);
if (val >= (int)SETPOINT_MIN && val <= (int)SETPOINT_MAX) savedTemps[i] = val;
else savedTemps[i] = 0;
}

delay(500);

myEnc.write(0);
oldPosition = myEnc.read();

needFilterReset = true;
resetActivity();
lastAdjustTime = millis();
lastUpdate = millis();

wdt_enable(WDTO_2S);
}

void loop() {
wdt_reset();
btn1.read();
btn2.read();

if (pendingSave) {
pendingSave = false;
saveTempSlot(pendingSaveSlot);
}

// Auto-off
if (!isSystemOff && (millis() - lastActivityTime > TIMEOUT_OFF)) {
isSystemOff = true;
needFilterReset = true;
heatFault = false;
highPwmSince = 0;
pwm = 0;
setHeaterPWM(pwm);
}

// --- ENCODER ---
long newPos = myEnc.read();
long deltaPos = newPos - oldPosition;
long absDelta = (deltaPos >= 0) ? deltaPos : -deltaPos;
if (absDelta >= 4 && (millis() - lastEncoderTime > 20)) {
wakeSystem();
long stepsLong = deltaPos / 4;
if (stepsLong > 5) stepsLong = 5;
if (stepsLong < -5) stepsLong = -5;
int steps = (int)stepsLong;
if (steps != 0) {
Setpoint += (float)steps * 5.0f;
if (Setpoint < SETPOINT_MIN) Setpoint = SETPOINT_MIN;
if (Setpoint > SETPOINT_MAX) Setpoint = SETPOINT_MAX;
oldPosition += (long)steps * 4L;
lastEncoderTime = millis();
lastAdjustTime = millis();
}
}

// --- CITIRE SENZOR SI CONTROL ---
if (millis() - lastUpdate >= SAMPLE_INTERVAL_MS) {
unsigned long nowSample = millis();
unsigned long rawDtMs = nowSample - lastUpdate;
lastUpdate = nowSample;

float dt = rawDtMs / 1000.0f;
if (rawDtMs < 50UL || rawDtMs > 10000UL) dt = SAMPLE_INTERVAL_MS / 1000.0f;
if (rawDtMs > 2000UL) needFilterReset = true;

if (isSystemOff) {
pwm = 0;
setHeaterPWM(pwm);
highPwmSince = 0;
} else {
bool hadSensorError = sensorError;

int prevPwm = pwm;
#if HEATER_QUIET_DURING_READ
setHeaterPWM(0);
delay(1);
#endif
float currentRead = tcReadCelsius() + CJ_OFFSET;
#if HEATER_QUIET_DURING_READ
setHeaterPWM(prevPwm);
#endif

bool invalid = isnan(currentRead) || isinf(currentRead)
|| (currentRead < MIN_VALID_TEMP)
|| (currentRead > MAX_VALID_TEMP);

if (invalid) {
pwm = 0;
setHeaterPWM(pwm);
outlierStreak = 0;
highPwmSince = 0;
invalidStreak++;
if (invalidStreak >= INVALID_ERR_COUNT) {
sensorError = true;
errSource = 1; // E1: senzor invalid/intrerupt
}
} else {
invalidStreak = 0;

if (!inputInitialized || needFilterReset || hadSensorError || dt > 2.0f) {
Input = currentRead;
lastInput = currentRead;
tempTrend = 0.0f;
inputInitialized = true;
needFilterReset = false;
outlierStreak = 0;
} else {
float predicted = lastInput + tempTrend * dt;
float upLim = predicted + (MAX_HEAT_RATE * dt + RATE_MARGIN);
float dnLim = predicted - (MAX_COOL_RATE * dt + RATE_MARGIN);

if (currentRead > upLim || currentRead < dnLim) {
outlierStreak++;
if (outlierStreak >= OUTLIER_ACCEPT_STREAK) {
float clamped = currentRead;
if (clamped > upLim) clamped = upLim;
if (clamped < dnLim) clamped = dnLim;
filterUpdate(clamped, dt);
}
} else {
outlierStreak = 0;
filterUpdate(currentRead, dt);
}
}

// --- E2: SUPRAÎNCĂLZIRE pe Input FILTRAT ---
if (!overheatLatch && (Input > MAX_SAFE_TEMP)) {
overheatLatch = true;
overheatSince = millis();
}
if (overheatLatch &&
(Input < (MAX_SAFE_TEMP - OVERHEAT_HYST)) &&
(millis() - overheatSince > OVERHEAT_MIN_OFF_MS)) {
overheatLatch = false;
}

if (overheatLatch) {
pwm = 0;
setHeaterPWM(pwm);
sensorError = true;
errSource = 2; // E2
highPwmSince = 0;
} else {
float dif = Setpoint - Input;
#if CALIBRATE_HOLD_BASE
pwm = pwm_hold_base;
#else
if (dif > DEADBAND) {
pwm = 255;
} else if (dif < -DEADBAND) {
pwm = 0;
} else {
int trendCorrection = (int)(-tempTrend * HOLD_TREND_GAIN);
int errorCorrection = (int)(dif * HOLD_ERROR_GAIN);
pwm = pwm_hold_base + trendCorrection + errorCorrection;
}
#endif
if (pwm < 0) pwm = 0;
if (pwm > 255) pwm = 255;

// --- E3: DEFECT HEATER, doar in faza de incalzire ---
if (heatFault) {
pwm = 0;
sensorError = true;
errSource = 3; // E3
if (millis() - faultSince > FAULT_RETRY_MS) {
heatFault = false;
highPwmSince = 0;
}
} else if (inputInitialized) {
bool heatingPhase = (Setpoint - Input) > HEAT_FAULT_GAP;
if (pwm >= 250 && heatingPhase) {
if (highPwmSince == 0) {
highPwmSince = millis();
tempAtHighPwm = Input;
} else if (millis() - highPwmSince > HEAT_FAULT_MS) {
if (Input >= tempAtHighPwm + HEAT_FAULT_RISE) {
highPwmSince = millis();
tempAtHighPwm = Input;
} else {
heatFault = true;
faultSince = millis();
}
}
} else {
highPwmSince = 0;
}
}

setHeaterPWM(pwm);

if (!heatFault && !overheatLatch && invalidStreak == 0) {
sensorError = false; // auto-recuperare
}
}
}
}
}

// --- LOGICA AFISARE ---
noInterrupts();
float inputSnap = Input;
float setpointSnap = Setpoint;
interrupts();

int valToDisplay;
if (isSystemOff) {
valToDisplay = -1; // STb
} else if (sensorError) {
valToDisplay = -110 - errSource; // -111..-113 => E1..E3
} else if (!inputInitialized || (millis() - lastAdjustTime < 2000UL)) {
valToDisplay = (int)(setpointSnap + 0.5f);
} else {
if (fabs(inputSnap - setpointSnap) < 5.1f) {
valToDisplay = (int)(setpointSnap + 0.5f);
} else {
valToDisplay = (int)(inputSnap + 0.5f);
}
}

noInterrupts();
unsigned long nowMs = millis();
bool blinkActive = (blinkStart != 0) && ((nowMs - blinkStart) < BLINK_TIME_MS);
bool blinkPhase = (((nowMs - blinkStart) / 100UL) % 2UL) == 0UL;
isrDisplayEnabled = blinkActive ? blinkPhase : true;
isrDisplayValue = valToDisplay;
interrupts();
}

vineri, 27 februarie 2026

Ceas cu termometru, display LCD, emitator WiFi

 Va salut !

Tot cu ajutorul AI am realizat un ceas, cu Wemos D1 mini, care afiseaza pe un dispay LCD si temperatura citita cu SHT40, transmisa din exteriorul locuintei, tot de un Wemos D1 mini . Dupa muuuulte teste si muuuuulte buguri rezolvate, pot spune ca  functioneaza foarte bine, nu am mai gasit nicio problema.Trecerea la ora de vara/iarna se face automat.

Ceasul propriu-zis il alimentez dintr-un alimentator de telefon mobil, de 5 volti. Emitatorul este alimentat dintr-un acumulator Li-Ion, a carui tensiune este citita periodic ; daca scade sub pragul critic, apare mesajul "ACCU" pe displayul ceasului. Consumul emitatorului este foarte mic pentru ca transmisia temperaturii se face o data la 5 minute, dupa care montajul intra in deep sleep. Alimentarea se face cu un IC specializat, HT7333. 

UPDATE 12/03/2026 (bug conectare corectat). 

UPDATE 28/03/2026 - Modificand putin codul si folosind aplicatia BLYNK pot vedea temperatura pe telefonul mobil, de oriunde. Inclusiv graficul evolutiei zilnice, saptamanala, lunara 


 

 


 

#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <TFT_eSPI.h>
#include <TimeLib.h>
#include <Timezone.h>
#include <NTPClient.h>
#include <WiFiUdp.h>

TFT_eSPI display = TFT_eSPI();
ESP8266WebServer server(80);
WiFiUDP ntpUDP;

// Actualizare NTP la 24 ore (86400000 ms) - offset 0, primim UTC
NTPClient timeClient(ntpUDP, "europe.pool.ntp.org", 0, 86400000);

const char* ssid = "your_SSID";
const char* password = "your_PSW";

// Reguli DST pentru București - ORDINE CORECTĂ: Standard (EET) apoi Daylight (EEST)
TimeChangeRule EET = {"EET", Last, Sun, Oct, 3, 120}; // Standard: UTC+2 (iarnă)
TimeChangeRule EEST = {"EEST", Last, Sun, Mar, 3, 180}; // Daylight: UTC+3 (vară)
Timezone Bucharest(EET, EEST); // Constructor: (standard, daylight)

const char* zileAbrev[] = {"DU", "LU", "MA", "MI", "JO", "VI", "SA"};
const char* luni[] = {"IAN", "FEB", "MAR", "APR", "MAI", "IUN", "IUL", "AUG", "SEP", "OCT", "NOV", "DEC"};

float receivedTemp = 0.0;
bool lowBattery = false;
bool timeInitialized = false;

// Variabilă pentru tracking reconectare WiFi
unsigned long lastWiFiAttempt = 0;
const unsigned long WIFI_RECONNECT_INTERVAL = 10000; // 10 secunde între încercări

void handleUpdate() {
if (server.hasArg("temp")) {
float v = server.arg("temp").toFloat();
if (v >= -50 && v <= 100) receivedTemp = v;
}
lowBattery = (server.hasArg("bat") && server.arg("bat") == "LOW");
server.send(200, "text/plain", "OK");
}

unsigned long prevDisplay = 0;


void displayInfo() {
time_t utc = timeClient.getEpochTime();

// Dacă nu am obținut NICIODATĂ ora corectă, afișăm "Sincronizare..."
if (!timeInitialized) {
display.fillScreen(TFT_BLACK);
display.setTextColor(TFT_WHITE, TFT_BLACK);
display.setFreeFont(&FreeSansBold12pt7b);
display.setTextDatum(MC_DATUM);
display.drawString("Sincronizare...", 120, 100);
return;
}

// AVEM ORA VALIDĂ - afișăm ceasul (chiar dacă WiFi pică între timp)
time_t local = Bucharest.toLocal(utc);
tmElements_t tm;
breakTime(local, tm);

int dd=tm.Day, mm=tm.Month-1, wd=tm.Wday-1, h=tm.Hour, m=tm.Minute, s=tm.Second;

static int lh=-1, lm=-1;
if (h!=lh || m!=lm) {
display.fillRect(20,80,220,80,TFT_BLACK);
display.setTextFont(7);
display.setTextColor(TFT_CYAN,TFT_BLACK);
display.setTextDatum(TR_DATUM);
display.drawString(String(h)+":"+(m<10?"0":"")+String(m), 175, 95);
lh=h; lm=m;
}

static String ls="";
String sec=(s<10?"0":"")+String(s);
if (sec!=ls) {
display.setFreeFont(&FreeSansBold12pt7b);
display.setTextDatum(MC_DATUM);
display.setTextColor(TFT_CYAN,TFT_BLACK);
display.fillRect(173,100,60,30,TFT_BLACK);
display.drawString(sec,190,105);
ls=sec;
}

static String ld="";
String dat=String(dd)+"-"+String(luni[mm]);
if (dat!=ld) {
display.setFreeFont(&FreeSansBold12pt7b);
display.setTextDatum(TL_DATUM);
display.setTextColor(TFT_GREEN,TFT_BLACK);
display.fillRect(10,25,100,25,TFT_BLACK);
display.drawString(dat,0,25);
ld=dat;
}

static String lt="";
String tmp=String(receivedTemp,1)+"'C";
if (tmp!=lt || receivedTemp==0.0) {
display.fillRect(233-90,25-2,90,25,TFT_BLACK);
display.setFreeFont(&FreeSansBold12pt7b);
display.setTextDatum(TR_DATUM);
display.setTextColor(TFT_PURPLE,TFT_BLACK);
display.drawString("'C",233,25);
display.setTextColor(TFT_YELLOW,TFT_BLACK);
display.drawString(String(receivedTemp,1),233-display.textWidth("'C"),25);
lt=tmp;
}

static bool lb=false;
if (lowBattery!=lb) {
display.fillRect(95,17,50,20,TFT_BLACK);
if (lowBattery) {
display.setTextFont(2);
display.setTextDatum(MC_DATUM);
display.setTextColor(TFT_RED,TFT_BLACK);
display.drawString("ACCU",120,25);
}
lb=lowBattery;
}

static int lw=-1;
int cw=(wd==0)?6:wd-1;
if (cw!=lw) {
display.fillRect(0,185,240,30,TFT_BLACK);
display.setFreeFont(&FreeSansBold9pt7b);
display.setTextDatum(MC_DATUM);
int pz[]={10,50,90,125,155,185,220};
for (int i=0;i<7;i++) {
int zi=(i+1)%7;
uint16_t c=(i==cw)?(zi==0||zi==6?TFT_RED:TFT_GREEN):TFT_DARKGREY;
display.setTextColor(c,TFT_BLACK);
display.drawString(zileAbrev[zi],pz[i],200);
}
lw=cw;
}
}

// =====================================================
// SETUP() - CORECTAT PENTRU ROUTER LENT
// =====================================================
void setup() {
display.init();
display.setRotation(3);
display.fillScreen(TFT_BLACK);
display.setTextColor(TFT_WHITE, TFT_BLACK);
display.setFreeFont(&FreeSansBold12pt7b);
display.setTextDatum(MC_DATUM);
display.drawString("Starting...", 120, 100);

// Curățare credențiale WiFi pentru conexiune proaspătă
WiFi.persistent(false);
delay(1000);
WiFi.disconnect(true);
delay(1000);
WiFi.mode(WIFI_OFF);
delay(1000);

WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);

// 1. Conectare la Router (WiFi) - EXTINS LA 60 SECUNDE PENTRU ROUTER LENT
int attempts = 0;
const int maxAttempts = 15; // 15 x 4s = 60 secunde (suficient pentru router lent)
while (WiFi.status() != WL_CONNECTED && attempts < maxAttempts) {
attempts++;
display.fillScreen(TFT_BLACK);
display.drawString("Conectare WiFi", 120, 100);
// display.drawString(String(attempts) + "/" + String(maxAttempts), 120, 140);
display.drawString("Astept router...", 120, 170);
delay(4000);
yield(); // ESP8266 WDT friendly
}

if (WiFi.status() != WL_CONNECTED) {
display.fillScreen(TFT_BLACK);
display.setTextColor(TFT_RED, TFT_BLACK);
display.drawString("Eroare WiFi", 120, 100);
display.drawString("Reboot in 10s...", 120, 140);
// Așteptăm 10 secunde înainte de reboot (poate routerul a pornit între timp)
for (int i = 10; i > 0; i--) {
display.drawString(String(i) + "s", 120, 170);
delay(4000);
yield();
}
ESP.restart();
}

// 2. WiFi conectat - Începem verificarea INTERNET (NTP)
display.fillScreen(TFT_BLACK);
display.drawString("Verificare NET", 120, 100);
display.drawString(" Astept NTP", 120, 140);
delay(4000);
timeClient.begin();

// 3. VERIFICARE INTERNET REAL prin NTP (maxim 20 secunde)
int ntpAttempts = 0;
while (!timeInitialized && ntpAttempts < 20) {
if (timeClient.update()) {
time_t epoch = timeClient.getEpochTime();
// Validare epoch : după 1 Ian 2020 și înainte de overflow 2038
if (epoch > 1577836800 && epoch < 2147483647) {
setTime(epoch); // Sincronizează TimeLib cu NTP!
timeInitialized = true;
break;
}
}
ntpAttempts++;
display.fillScreen(TFT_BLACK);
display.drawString("Sincronizare NTP", 120, 100);
display.drawString(String(ntpAttempts) + "/20", 120, 140);
delay(1000);
yield(); // ESP8266 WDT friendly
}

if (!timeInitialized) {
// NTP a eșuat - nu avem internet real
display.fillScreen(TFT_BLACK);
display.setTextColor(TFT_RED, TFT_BLACK);
display.drawString("Eroare NET", 120, 100);
display.drawString("NTP Esuat", 120, 130);
display.drawString("Reboot in 5s", 120, 160);
for (int i = 5; i > 0; i--) {
display.drawString(String(i) + "s", 120, 190);
delay(1000);
yield();
}
ESP.restart();
}

// 4. SUCCES - Acum afișăm "WiFi Conectat" și IP (DOAR după NTP OK)
display.fillScreen(TFT_BLACK);
display.setTextColor(TFT_GREEN, TFT_BLACK);
display.drawString("WiFi Conectat", 120, 80);
display.setTextColor(TFT_WHITE, TFT_BLACK);
display.drawString(WiFi.SSID(), 120, 110);
display.drawString(WiFi.localIP().toString(), 120, 140);
display.drawString("Start ceas...", 120, 180);

server.on("/update", handleUpdate);
server.begin();
delay(4000);

// 5. Curățare ecran înainte de loop
display.fillScreen(TFT_BLACK);
}

// =====================================================
// LOOP() - ADĂUGATĂ RECONECTARE WIFI AUTOMATĂ
// =====================================================
void loop() {
server.handleClient();

// Reconectare automată dacă WiFi se pierde
if (WiFi.status() != WL_CONNECTED) {
if (millis() - lastWiFiAttempt > WIFI_RECONNECT_INTERVAL) {
display.setTextColor(TFT_ORANGE, TFT_BLACK);
display.setTextDatum(MC_DATUM);
display.setFreeFont(&FreeSansBold9pt7b);
display.drawString("Reconectare...", 120, 220);

WiFi.reconnect(); // Încearcă reconectarea
lastWiFiAttempt = millis();
}
yield(); // ESP8266 WDT friendly
return; // Skip rest until reconnected
}

// WiFi este conectat - actualizăm NTP (librăria gestionează intervalul de 24h)
if (timeClient.update()) {
time_t epoch = timeClient.getEpochTime();
if (epoch > 1577836800 && epoch < 2147483647) {
setTime(epoch); // Menține TimeLib sincronizat
timeInitialized = true;
}
}

// Afișare ceas la fiecare secundă
if (millis() - prevDisplay >= 1000) {
prevDisplay = millis();
displayInfo();
}

yield(); // ESP8266 WDT friendly - obligatoriu în loop
}
<10 175="" 95="" lh="h;" lm="m;" ls="" m="" sec="(utc" secunde="" static="" string="" tring=""><10 dat="(utc" display.drawstring="" display.fillrect="" display.setfreefont="" display.settextcolor="" display.settextdatum="" if="" ld="" ls="sec;" reesansbold12pt7b="" s="" sec="" static="" string="" tring=""><7 -="" 100="" 120="" 140="" 1="" 1ms="" 2="" a="" actualizare="" afi="" am="" attempts="" blocheaz="" bucla="" c="" conectare="" const="" critic:="" cu="" curent="" da="" dac="" de="" delay="" display.drawstring="" display.fillscreen="" display.init="" display.setfreefont="" display.setrotation="" display.settextcolor="" display.settextdatum="" doar="" eaz="" eboot...="" ecran="" else="" epuizat="" erial.print="" eroare="" esp.restart="" ex:="" excep="" f="" fiecare="" handleupdate="" i="" if="" ifi.localip="" ifi.status="" ii="" int="" intervale="" ional:="" it="" itera="" la="" loop="" lw="cw;" m="" mai="" maxattempts="" millis="" mult="" ncerc="" ncercarea:="" ncercarea="" niciodat="" non-blocking="" ntp="" ntre="" nu="" onectare="" onectat="" op="" password="" pe="" pentru="" prevntp="" pz="" r="" reesansbold12pt7b="" reu="" ri="" ro="" roare="" routerului="" sau="" secund="" serial.begin="" serial.println="" server.begin="" server.handleclient="" server.on="" setup="" ssid="" string="" teapt="" tentativele="" tft_black="" timeclient.begin="" timeclient.update="" timp="" tostring="" tring="" u="" uint16_t="" update="" verific="" void="" while="" wifi...="" wifi.begin="" wifi.mode="" wifi="" wl_connected="" zi="" zileabrev="">

joi, 26 februarie 2026

Statie meteo cu Wemos D1 mini si ST7735

Va salut ! Fiindca o lunga perioada de timp am fost in concediu medical, dupa o afectiune oncologica ce a necesitat operatie, in timpul liber disponibil am lucrat la o serie de proiecte, care sa-mi tina mintea distrasa de la alte ganduri ... Cu ajutorul AI (Vercel, Perplexity, Gemini, Qwen, Le Chat) am conceput si finalizat, dupa multe, multe incercari si aceasta statie meteo color. Functioneaza foarte bine, prietenul meu, Dragos, o are "activa" si e multumit de ea. Nota : am experimentat cu datele meteo de pe toate siteurile care pun la dispozitie apiKey. Nu sunt doua la fel !! Exista mari variatii, in special in privinta temperaturii, dar, la unele siteuri, si la starea vremii propriu-zisa (afara era soare, senin si prognoza era innorat, sanse de ploaie :( ). Acesta este si motivul pentru care eu nu folosesc aceasta statie, prefer ceasul cu termometru (ce poate fi vazut tot aici, pe blog). Cine doreste poate face statia, ideea este sa va obtineti propriul apiKey (google_it) si sa va setati locatia. Succes !
//Statie meteo cu ST7735


#include <SPI.h>
#include <TimeLib.h>
#include <ArduinoJson.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <time.h>
#include "OpenWeatherMapCurrent.h"
#include <WiFiUdp.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeMono12pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeSans12pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold9pt7b.h>