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>

miercuri, 25 februarie 2026

Ceas Matrix cu termometru

 Va salut !

Si inca un montaj cu excelentul Wemos D1 mini : ceas cu matrice de leduri, cu senzor de temperatura DS18B20.  Are efect de scroll a afisarii. Testat, functioneaza perfect. L-am montat in bucatarie, iar senzorul de temperatura l-am scos in balcon. Succes !

 


 




 

 

 

// Ceas matrice LED cu ora automata vara/iarna + DS18B20 
// Conexiuni : DS18B20 la D2 ; matrice DIN - D8 ; CS - D7 ; CLK - D6

#include <ESP8266WiFi.h>
#include <time.h>
#include <MD_MAX72xx.h>
#include <SPI.h>
#include <OneWire.h>
#include <DallasTemperature.h>

luni, 23 februarie 2026

Ceas LCD 2x16 cu termometru

 Va salut !

Si inca un montaj cu Wemos D1 mini (sper ca nu v-am plictisit !). Un ceas cu termometru (DS18B20), cu display LCD 2x16, conectare la WiFi. Functioneaza perfect, de multa vreme. 

 


//ceas cu termometru, ora iarna/vara, cu modificari AI
//
// vers.febr.2026-bug fixed
//
#ifdef ESP8266
#include <ESP8266WiFi.h>
#else
#include <WiFi.h>
#endif
#include <Wire.h>
#include <DallasTemperature.h>
#include <OneWire.h>
#include <time.h>
#include <LiquidCrystal_I2C.h>

// --- CONFIGURARE ---
#define ONE_WIRE_BUS D3
const char* ssid = "your_SSID";
const char* password = "your_PSW";
const char* TZ_INFO = "EET-2EEST,M3.5.0/3,M10.5.0/4";
#define WIFI_RETRY_LIMIT 15
#define WIFI_RETRY_DELAY 1000

// Obiecte Globale
LiquidCrystal_I2C lcd(0x27, 16, 2);
OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

byte customDegreeC[] = {
0x08, 0x14, 0x08, 0x07,
0x08, 0x08, 0x07, 0x00
};

tm timeinfo;
time_t now;
String StrTempC = "---";
int prevSec = -1;
float tempSum = 0;
int tempCount = 0;
#define TEMP_SAMPLES 5
unsigned long lastSensorMillis = 0;

char* getDOW(uint8_t tm_wday) {
switch (tm_wday) {
case 1: return (char*)"Luni"; case 2: return (char*)"Marti"; case 3: return (char*)"Miercuri";
case 4: return (char*)"Joi"; case 5: return (char*)"Vineri"; case 6: return (char*)"Sambata";
case 0: return (char*)"Duminica"; default: return (char*)"Error";
}
}

void showTimeDateTemp(tm *localTime) {
char oraStr[9];
sprintf(oraStr, "%02d:%02d:%02d", localTime->tm_hour, localTime->tm_min, localTime->tm_sec);
String tVal = StrTempC;
while (tVal.length() < 5) tVal = " " + tVal;
int spatiiMijloc1 = 16 - 8 - 6;
lcd.setCursor(0, 0);
lcd.print(oraStr);
for(int i=0; i < spatiiMijloc1; i++) lcd.print(" ");
lcd.print(tVal);
if (StrTempC != "Err") lcd.write(0); else lcd.print(" ");

char dataStr[6];
sprintf(dataStr, "%02d/%02d", localTime->tm_mday, localTime->tm_mon + 1);
char* dow = getDOW(localTime->tm_wday);
int spatiiMijloc2 = 16 - 5 - strlen(dow);
lcd.setCursor(0, 1);
lcd.print(dataStr);
for(int i=0; i < spatiiMijloc2; i++) lcd.print(" ");
lcd.print(dow);
}

void setup() {
sensors.begin();
sensors.setWaitForConversion(false); // REPARĂ BUG-UL: Nu mai blochează procesorul
Serial.begin(115200);
Wire.begin(4, 5);
delay(1000);
lcd.init();
lcd.backlight();
lcd.clear();
lcd.createChar(0, customDegreeC);

lcd.setCursor(0, 0);
lcd.print("Conectare WiFi..");
WiFi.begin(ssid, password);
int incercari = 0;
while (WiFi.status() != WL_CONNECTED && incercari < WIFI_RETRY_LIMIT) {
delay(WIFI_RETRY_DELAY);
lcd.setCursor(0, 1);
lcd.print("Incercari: "); lcd.print(incercari + 1);
incercari++;
}

if (WiFi.status() == WL_CONNECTED) {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("WiFi:"); lcd.print(WiFi.SSID());
lcd.setCursor(0, 1); lcd.print("IP:"); lcd.print(WiFi.localIP());
delay(3000);
} else {
lcd.clear();
lcd.setCursor(0, 0); lcd.print("Eroare WiFi !");
lcd.setCursor(0, 1); lcd.print("Resetare ...");
delay(5000);
ESP.restart();
}

lcd.clear();
lcd.setCursor(0, 0);
lcd.print("Sincro timp...");
configTime(0, 0, "pool.ntp.org", "ro.pool.ntp.org");
setenv("TZ", TZ_INFO, 1);
tzset();

int ntp_timeout = 0;
time_t acum = time(nullptr);
struct tm *ti = localtime(&acum);
while (ti->tm_year < 120 && ntp_timeout < 30) {
delay(1000); acum = time(nullptr); ti = localtime(&acum);
lcd.setCursor(ntp_timeout % 16, 1); lcd.print(".");
ntp_timeout++;
}
if (ti->tm_year < 120) ESP.restart();
lcd.clear();
}

void loop() {
time(&now);
localtime_r(&now, &timeinfo);

// Afisare imediata la schimbarea secundei (fara blocaj)
if (timeinfo.tm_sec != prevSec) {
showTimeDateTemp(&timeinfo);
prevSec = timeinfo.tm_sec;
}

// Gestionare senzor fara a opri timpul in loc
if (millis() - lastSensorMillis >= 2000) {
float t = sensors.getTempCByIndex(0); // Citeste valoarea deja calculata
if (t <= -126.0 || t >= 85.0) {
StrTempC = "Err";
tempCount = 0; tempSum = 0;
} else {
tempSum += t;
tempCount++;
if (tempCount >= TEMP_SAMPLES) {
StrTempC = String(tempSum / TEMP_SAMPLES, 1);
tempSum = 0; tempCount = 0;
}
}
sensors.requestTemperatures(); // Porneste noua conversie "in fundal"
lastSensorMillis = millis();
}
}