/* Termómetro IR – ESP32-C3 + MLX90614 + OLED SSD1306 (I2C)
* RITSA Electrónica – Versión técnica para post
* Pines (ESP32-C3):
* SDA = GPIO5
* SCL = GPIO6
* BTN = GPIO10 (INPUT_PULLUP, activo en LOW)
*
* Librerías:
* Adafruit_MLX90614, Adafruit_SSD1306, Adafruit_GFX, Wire
*/
#include
#include
#include
#include
// ---------- Configuración de pines ----------
static constexpr int PIN_SDA = 5;
static constexpr int PIN_SCL = 6;
static constexpr int PIN_BTN = 10;
// ---------- OLED ----------
#define OLED_ADDR 0x3C // Cambiar si tu módulo usa otra dirección
#define OLED_WIDTH 128
#define OLED_HEIGHT 64
Adafruit_SSD1306 display(OLED_WIDTH, OLED_HEIGHT, &Wire, -1);
// ---------- MLX90614 ----------
Adafruit_MLX90614 mlx = Adafruit_MLX90614(); // I2C
// ---------- Control de muestreo / filtro ----------
static constexpr uint16_t LOOP_FAST_MS = 150; // modo rápido
static constexpr uint16_t LOOP_SLOW_MS = 500; // modo lento
bool fastMode = true;
static constexpr size_t AVG_WIN = 8; // ventana de media móvil
float bufObj[AVG_WIN] = {0};
float bufAmb[AVG_WIN] = {0};
size_t idx = 0;
bool bufFilled = false;
// ---------- Botón (antirrebote) ----------
static constexpr uint32_t DEBOUNCE_MS = 30;
static constexpr uint32_t LONGPRESS_MS = 1200;
bool btnPrev = true;
uint32_t tLast = 0;
uint32_t tPress = 0;
bool holdFreeze = false;
// Utilidades
static inline float c_to_f(float c) { return c * 9.0f / 5.0f + 32.0f; }
float movingAvg(float *b, size_t n, bool filled) {
size_t count = filled ? n : idx;
if (count == 0) return 0;
float acc = 0;
for (size_t i = 0; i < count; ++i) acc += b[i];
return acc / (float)count;
}
void drawScreen(float objC, float ambC) {
display.clearDisplay();
// Header
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("RITSA - IR Thermometer"));
// Separador
display.drawLine(0, 10, 127, 10, SSD1306_WHITE);
// Temperaturas
display.setTextSize(2);
display.setCursor(0, 16);
display.print(F("Obj: "));
display.print(objC, 1);
display.print(F("C"));
display.setCursor(0, 36);
display.print(F("Obj: "));
display.print(c_to_f(objC), 1);
display.print(F("F"));
display.setTextSize(1);
display.setCursor(0, 56);
display.print(F("Amb: "));
display.print(ambC, 1);
display.print(F("C ("));
display.print(c_to_f(ambC), 1);
display.print(F("F)"));
// Estado
if (holdFreeze) {
display.setCursor(92, 56);
display.print(F("[HOLD]"));
} else {
display.setCursor(92, 56);
display.print(fastMode ? F("[FAST]") : F("[SLOW]"));
}
display.display();
}
void setup() {
pinMode(PIN_BTN, INPUT_PULLUP);
// I2C en pines definidos
Wire.begin(PIN_SDA, PIN_SCL);
// OLED
if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
// Si falla, no bloqueamos; mostramos por Serial
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE);
display.setCursor(0, 0);
display.println(F("Init I2C / OLED..."));
display.display();
// MLX90614
if (!mlx.begin()) {
display.clearDisplay();
display.setCursor(0, 0);
display.println(F("MLX90614 not found"));
display.display();
delay(1500);
} else {
display.setCursor(0, 10);
display.println(F("MLX90614 OK"));
display.display();
delay(600);
}
}
void loop() {
// --- Gestión de botón con antirrebote ---
bool btn = digitalRead(PIN_BTN); // true=HIGH (no pulsado), false=LOW (pulsado)
uint32_t now = millis();
if (btn != btnPrev) {
tLast = now; // cambió de estado: inicia ventana de debounce
btnPrev = btn;
}
// estable tras DEBOUNCE_MS
if (now - tLast >= DEBOUNCE_MS) {
static bool btnStable = true;
static bool btnStablePrev = true;
btnStable = btn; // estado estable
// flanco de bajada: inicio de pulsación
if (btnStable == LOW && btnStablePrev == HIGH) {
tPress = now;
}
// flanco de subida: fin de pulsación
if (btnStable == HIGH && btnStablePrev == LOW) {
uint32_t dur = now - tPress;
if (dur >= LONGPRESS_MS) {
// pulsación larga: conmutar velocidad
fastMode = !fastMode;
} else {
// pulsación corta: congela/retoma
holdFreeze = !holdFreeze;
}
}
btnStablePrev = btnStable;
}
static uint32_t tLoop = 0;
uint16_t loopMs = fastMode ? LOOP_FAST_MS : LOOP_SLOW_MS;
if (now - tLoop < loopMs) return;
tLoop = now;
// --- Adquisición y filtrado ---
static float objC = 0, ambC = 0;
if (!holdFreeze) {
float newObj = mlx.readObjectTempC();
float newAmb = mlx.readAmbientTempC();
// Sanitizar lecturas
if (!isnan(newObj) && newObj > -70 && newObj < 500) bufObj[idx] = newObj;
if (!isnan(newAmb) && newAmb > -70 && newAmb < 500) bufAmb[idx] = newAmb;
idx++;
if (idx >= AVG_WIN) { idx = 0; bufFilled = true; }
objC = movingAvg(bufObj, AVG_WIN, bufFilled);
ambC = movingAvg(bufAmb, AVG_WIN, bufFilled);
}
// --- Render UI ---
drawScreen(objC, ambC);
}
Puntos técnicos relevantes del firmware
I²C explícito: Wire.begin(5, 6) fija SDA/SCL en GPIO5/6 (por defecto del proyecto).
Filtro de media móvil (AVG_WIN = 8): mejora estabilidad visual y reduce jitter.
Antirrebote con ventana DEBOUNCE_MS=30 y detección de pulsación larga (LONGPRESS_MS=1200).
Rangos válidos para descartar valores anómalos: −70…500 °C (pueden ajustarse a tu caso).
Refresco dual: FAST (150 ms) para apuntado, SLOW (500 ms) para estabilidad / ahorro.
OLED: fuente conmutada interna SSD1306_SWITCHCAPVCC (la más común). Dirección por defecto 0x3C.
✅ Validaciones y pruebas recomendadas
Scan I²C: si no ves lecturas, corré un I²C scanner para verificar direcciones y continuidad de SDA/SCL.
Consumo: midí corriente con y sin Wi-Fi activo para dimensionar batería (picos del ESP32-C3 pueden superar 200–300 mA en Tx).
Térmica: el MLX90614 es sensible a corrientes de aire y temperatura del PCB; evitá montarlo cerca de reguladores calientes.
Calibración: para offsets leves, aplicá una corrección en firmware (p. ej., objC += kOffset;).