You dont have javascript enabled! Please enable it!

Caso práctico: Logger de energía Modbus Ethernet ESP32 RS485

Caso práctico: Logger de energía Modbus Ethernet ESP32 RS485 — hero

Objetivo y caso de uso

Qué construirás: Un sistema de registro de energía utilizando un ESP32-Ethernet-Kit como maestro Modbus RTU para capturar y exponer datos de consumo energético a través de Ethernet.

Para qué sirve

  • Monitoreo en tiempo real de consumo energético en instalaciones industriales.
  • Registro de datos históricos para análisis de eficiencia energética.
  • Integración con sistemas de gestión de energía mediante protocolo Modbus.
  • Control remoto de dispositivos eléctricos basado en datos de consumo.
  • Generación de alertas sobre consumos anómalos a través de MQTT.

Resultado esperado

  • Captura de datos de consumo en tiempo real con una frecuencia de 1 segundo.
  • Latencia de respuesta del sistema inferior a 100 ms en la comunicación Modbus.
  • Exposición de datos a través de Ethernet con un throughput de 500 paquetes/s.
  • Registro de datos con un margen de error inferior al 1% en comparación con medidores de referencia.
  • Generación de informes semanales sobre consumo energético con métricas detalladas.

Público objetivo: Ingenieros y desarrolladores de sistemas embebidos; Nivel: Avanzado

Arquitectura/flujo: Comunicación Modbus RTU sobre RS485 hacia el ESP32, procesamiento de datos y exposición a través de Ethernet.

Nivel: Avanzado

Prerrequisitos

Sistemas operativos soportados y versiones probadas

  • Windows 11 Pro 23H2 (build 22631)
  • Ubuntu 22.04.5 LTS (Jammy)
  • macOS 14.5 (Sonoma)

En los tres entornos se validará compilación, flasheo y monitor serie.

Toolchain exacta (versiones fijas)

  • PlatformIO Core (CLI): 6.1.14
  • Python: 3.11.6
  • Plataforma Espressif32 (PlatformIO): espressif32@6.9.0
  • Framework Arduino-ESP32: framework-arduinoespressif32@3.20014.0 (equivale a Arduino-ESP32 2.0.14)
  • Toolchain GCC Xtensa (ESP32): toolchain-xtensa-esp32@8.4.0+2021r2-patch5 (gcc 8.4.0)
  • esptool.py (empacado por PlatformIO): tool-esptoolpy@1.40501.0 (esptool.py 4.5.1)
  • OpenOCD-ESP32 (opcional para debug JTAG): 0.12.0-esp32-20230921 (no necesario para este caso)
  • Librerías de proyecto:
  • 4-20ma/ModbusMaster@2.0.1
  • bblanchon/ArduinoJson@6.21.2

Drivers USB-UART

  • El ESP32-Ethernet-Kit suele integrar un puente USB-UART Silicon Labs CP2102/CP210x.
  • Alternativamente, algunos kits pueden usar CH34x.
  • Instalar drivers:
  • Windows: CP210x Universal Windows Driver v10.1.x o CH34x Driver v3.6.x
  • macOS: CP210x VCP Driver 6.0.x (firmado) o CH34x 1.7.x
  • Linux: normalmente no requiere instalación; verificar permisos udev.

Requisitos de red

  • Red Ethernet con DHCP habilitado (recomendado para primeras pruebas).
  • Cable RJ45 Cat5e o superior.
  • Opcional: IP estática conocida para pruebas posteriores.

Verificaciones rápidas

  • Comandos de comprobación (ejecutar en terminal):
  • Windows (PowerShell):
    • pio –version
    • python –version
  • Linux/macOS:
    • pio –version
    • python3 –version
  • Versiones esperadas:
  • PlatformIO Core, salida similar a: PlatformIO Core, version 6.1.14
  • Python 3.11.6

Materiales

  • Placa: ESP32-Ethernet-Kit + LAN8720 + MAX3485 RS485 (exactamente este modelo de dispositivo)
  • ESP32-Ethernet-Kit (ESP32-WROOM-32E; PHY LAN8720 en modo RMII; reloj por GPIO17)
  • Transceptor RS485: MAX3485 (módulo TTL↔RS485, 3.3 V)
  • Cable micro-USB de datos (no solo carga)
  • Cable Ethernet RJ45
  • Fuente de alimentación 5 V (si no se alimenta por USB)
  • Medidor de energía con interfaz Modbus RTU/RS485 (p. ej., Eastron SDM120/SDM230/SDM630 o similar)
  • Resistencias de terminación RS485 de 120 Ω (en ambos extremos si el bus lo requiere)
  • Cables dupont macho-hembra para conexión del MAX3485 a GPIOs del ESP32
  • PC con uno de los OS soportados y PlatformIO instalado

Notas:
– Mantendremos coherencia total con el modelo “ESP32-Ethernet-Kit + LAN8720 + MAX3485 RS485” en conexiones, código y comandos.
– El objetivo del proyecto es “modbus-energy-logger-ethernet”: leer parámetros energéticos vía Modbus RTU/RS485 y publicarlos por Ethernet (HTTP/JSON), con trazas y validación.

Preparación y conexión

Reglas udev (Linux) y puertos serie

  • En Linux, añadir reglas udev si no existen:
# Copiar reglas de PlatformIO
wget https://raw.githubusercontent.com/platformio/platformio-core/develop/scripts/99-platformio-udev.rules -O /tmp/99-platformio-udev.rules
sudo cp /tmp/99-platformio-udev.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules
sudo udevadm trigger
  • Desconectar y reconectar el ESP32-Ethernet-Kit por USB.
  • Verificar puerto (ej.: /dev/ttyUSB0 o /dev/tty.SLAB_USBtoUART en macOS, COMx en Windows).

Mapeo de pines y conexión física

Tabla de pines relevantes en ESP32-Ethernet-Kit (LAN8720 y RS485 con MAX3485):

Función ESP32 GPIO Uso/Nota
ETH RMII MDC GPIO23 Línea de gestión PHY
ETH RMII MDIO GPIO18 Línea de gestión PHY
ETH PHY dirección 0 PHY_ADDR = 0 (por defecto en el kit)
ETH clock RMII GPIO17 Modo ETH_CLOCK_GPIO17_OUT (50 MHz output)
ETH TXD0/TXD1/TXEN 19/22/21 Cableadas en la placa (no cambiar)
ETH RXD0/RXD1/CRS_DV 25/26/27 Cableadas en la placa (no cambiar)
UART2 TX (RS485 DI) GPIO4 Conectar a DI del MAX3485
UART2 RX (RS485 RO) GPIO5 Conectar a RO del MAX3485
RS485 DE/RE (control TX/RX) GPIO15 Conectar a DE y RE unidos (DE=RE) en el MAX3485
3V3 3V3 Alimentación del MAX3485 (3.3 V)
GND GND Masa común con MAX3485 y medidor
USB-UART CP210x Programación y monitor serie

Conexiones RS485 (MAX3485 ↔ ESP32-Ethernet-Kit):
– MAX3485 DI → ESP32 GPIO4 (TX2)
– MAX3485 RO → ESP32 GPIO5 (RX2)
– MAX3485 DE y RE unidos → ESP32 GPIO15
– MAX3485 VCC → 3.3 V del ESP32-Ethernet-Kit
– MAX3485 GND → GND del ESP32-Ethernet-Kit
– RS485 A ↔ A (D+) del medidor
– RS485 B ↔ B (D−) del medidor
– Terminar bus con 120 Ω en los extremos si la topología lo requiere.

Conexión Ethernet:
– Conectar cable RJ45 al puerto del LAN8720 del ESP32-Ethernet-Kit y al switch/router con DHCP.

Atención a GPIOs reservados por RMII:
– No reusar 17, 18, 19, 21, 22, 23, 25, 26, 27 para otros fines.
– Evitar manipular GPIO0/2/12/15 en el arranque (son strap); en nuestro caso usaremos GPIO15 para DE/RE, manteniéndolo en LOW por defecto durante el boot.

Código completo

A continuación se muestra un proyecto funcional con PlatformIO usando Arduino-ESP32 2.0.14 y las librerías indicadas. Implementa:
– Inicialización Ethernet (LAN8720) con DHCP.
– Maestro Modbus RTU por RS485 (MAX3485) para leer parámetros típicos de un medidor (direcciones compatibles con SDM120/230/630 y similares).
– Servidor HTTP (WebServer) con endpoints:
– / (tabla HTML)
– /json (salida JSON)
– /metrics (formato estilo Prometheus, opcional)
– Bucle de sondeo periódico y cacheo de últimas lecturas.

platformio.ini

[env:esp32-ethernet-kit]
platform = espressif32@6.9.0
board = esp32-ethernet-kit
framework = arduino

platform_packages =
    platformio/tool-esptoolpy @ 1.40501.0
    platformio/toolchain-xtensa-esp32 @ 8.4.0+2021r2-patch5
    platformio/framework-arduinoespressif32 @ 3.20014.0

monitor_speed = 115200

lib_deps =
    4-20ma/ModbusMaster @ 2.0.1
    bblanchon/ArduinoJson @ 6.21.2

src/main.cpp

#include <Arduino.h>
#include <ETH.h>
#include <WebServer.h>
#include <ModbusMaster.h>
#include <ArduinoJson.h>

// -------------------- Configuración de hardware --------------------
static constexpr int PIN_RS485_TX = 4;     // UART2 TX -> MAX3485 DI
static constexpr int PIN_RS485_RX = 5;     // UART2 RX -> MAX3485 RO
static constexpr int PIN_RS485_REDE = 15;  // RS485 DE/RE (unidos) -> control TX/RX

// LAN8720 (ESP32-Ethernet-Kit) - RMII
static constexpr int ETH_PHY_ADDR = 0;               // Dirección por defecto del LAN8720
static constexpr int ETH_POWER_PIN = -1;             // Control de energía no usado (interno)
static constexpr int ETH_MDC_PIN = 23;               // MDC
static constexpr int ETH_MDIO_PIN = 18;              // MDIO
static constexpr eth_phy_type_t ETH_PHY_TYPE = ETH_PHY_LAN8720;
static constexpr eth_clock_mode_t ETH_CLK_MODE = ETH_CLOCK_GPIO17_OUT;

// -------------------- Configuración Modbus y medidor --------------------
// Parámetros de bus Modbus RTU típicos (ajustar si su medidor usa otros):
static uint8_t MODBUS_SLAVE_ID = 1;     // Dirección del medidor en el bus
static uint32_t MODBUS_BAUD = 9600;     // Baud rate
static uint16_t MODBUS_TIMEOUT_MS = 300; // Timeout por transacción

// Registros de ejemplo (Eastron SDM* Input Registers: floats 32-bit, 2 registros cada uno)
static constexpr uint16_t REG_VOLTAGE = 0x0000;      // V (2 regs)
static constexpr uint16_t REG_CURRENT = 0x0006;      // A (2 regs)
static constexpr uint16_t REG_ACTIVE_POWER = 0x000C; // W (2 regs)
static constexpr uint16_t REG_POWER_FACTOR = 0x001E; // PF (2 regs)
static constexpr uint16_t REG_FREQUENCY = 0x0046;    // Hz (2 regs)
static constexpr uint16_t REG_IMPORT_ACTIVE_ENERGY = 0x0048; // kWh (2 regs)

// Endianness común en muchos medidores SDM: word swap (lo-hi o hi-lo).
// Ajuste a true si el medidor requiere intercambiar palabras.
static bool FLOAT_WORD_SWAP = true;

// Periodo de sondeo
static uint32_t POLL_INTERVAL_MS = 1000;

// -------------------- Objetos globales --------------------
HardwareSerial RS485(2);        // UART2
ModbusMaster modbus;            // Maestro Modbus RTU
WebServer server(80);

// Variables de estado Ethernet
volatile bool eth_connected = false;
IPAddress eth_ip;

// Cache de lecturas
struct EnergyData {
  float voltage = NAN;
  float current = NAN;
  float active_power = NAN;
  float power_factor = NAN;
  float frequency = NAN;
  float import_active_energy = NAN;
  uint64_t last_update_ms = 0;
  uint32_t ok_count = 0;
  uint32_t err_count = 0;
} data;

// -------------------- RS485 control --------------------
void preTransmission() {
  digitalWrite(PIN_RS485_REDE, HIGH); // Habilitar TX
  delayMicroseconds(10);
}
void postTransmission() {
  delayMicroseconds(10);
  digitalWrite(PIN_RS485_REDE, LOW); // Volver a RX
}

// -------------------- Utilidades Modbus --------------------
bool readFloatInputRegister(uint16_t reg, float &out) {
  modbus.setSlave(MODBUS_SLAVE_ID);
  uint8_t result = modbus.readInputRegisters(reg, 2);
  if (result == modbus.ku8MBSuccess) {
    uint16_t w0 = modbus.getResponseBuffer(0);
    uint16_t w1 = modbus.getResponseBuffer(1);
    uint32_t raw = 0;
    if (FLOAT_WORD_SWAP) {
      raw = ((uint32_t)w1 << 16) | w0;
    } else {
      raw = ((uint32_t)w0 << 16) | w1;
    }
    float f;
    memcpy(&f, &raw, sizeof(f));
    out = f;
    return true;
  }
  return false;
}

// -------------------- HTTP Handlers --------------------
String htmlEscape(const String &s) {
  String r;
  r.reserve(s.length());
  for (char c : s) {
    switch (c) {
      case '&': r += "&amp;"; break;
      case '<': r += "&lt;"; break;
      case '>': r += "&gt;"; break;
      case '"': r += "&quot;"; break;
      default: r += c;
    }
  }
  return r;
}

void handleRoot() {
  String ip = eth_connected ? eth_ip.toString() : String("desconectado");
  String page;
  page.reserve(2048);
  page += "<!doctype html><html><head><meta charset='utf-8'><title>modbus-energy-logger-ethernet</title>";
  page += "<meta name='viewport' content='width=device-width,initial-scale=1'>";
  page += "<style>body{font-family:system-ui,Arial;margin:20px}table{border-collapse:collapse}td,th{border:1px solid #ccc;padding:6px 10px}</style>";
  page += "</head><body>";
  page += "<h1>modbus-energy-logger-ethernet</h1>";
  page += "<p>IP (ETH): " + htmlEscape(ip) + "</p>";
  page += "<table><tr><th>Métrica</th><th>Valor</th><th>Unidad</th></tr>";
  page += "<tr><td>Tensión</td><td>" + String(data.voltage, 3) + "</td><td>V</td></tr>";
  page += "<tr><td>Corriente</td><td>" + String(data.current, 3) + "</td><td>A</td></tr>";
  page += "<tr><td>Potencia Activa</td><td>" + String(data.active_power, 3) + "</td><td>W</td></tr>";
  page += "<tr><td>Factor de Potencia</td><td>" + String(data.power_factor, 3) + "</td><td>pf</td></tr>";
  page += "<tr><td>Frecuencia</td><td>" + String(data.frequency, 3) + "</td><td>Hz</td></tr>";
  page += "<tr><td>Energía Importada</td><td>" + String(data.import_active_energy, 3) + "</td><td>kWh</td></tr>";
  page += "</table>";
  page += "<p>Última actualización: " + String(data.last_update_ms) + " ms desde boot</p>";
  page += "<p>OK: " + String(data.ok_count) + " | ERR: " + String(data.err_count) + "</p>";
  page += "<p><a href='/json'>/json</a> | <a href='/metrics'>/metrics</a></p>";
  page += "</body></html>";
  server.send(200, "text/html; charset=utf-8", page);
}

void handleJSON() {
  StaticJsonDocument<512> doc;
  doc["ip"] = eth_connected ? eth_ip.toString() : "desconectado";
  doc["uptime_ms"] = millis();
  doc["ok_count"] = data.ok_count;
  doc["err_count"] = data.err_count;

  JsonObject m = doc.createNestedObject("metrics");
  m["voltage_V"] = data.voltage;
  m["current_A"] = data.current;
  m["active_power_W"] = data.active_power;
  m["power_factor"] = data.power_factor;
  m["frequency_Hz"] = data.frequency;
  m["import_active_energy_kWh"] = data.import_active_energy;
  m["last_update_ms"] = data.last_update_ms;

  String out;
  serializeJsonPretty(doc, out);
  server.send(200, "application/json; charset=utf-8", out);
}

void handleMetrics() {
  String txt;
  txt.reserve(512);
  txt += "# HELP voltage_V Tensión de línea (V)\n# TYPE voltage_V gauge\n";
  txt += "voltage_V " + String(isnan(data.voltage)?0:data.voltage, 6) + "\n";
  txt += "# HELP current_A Corriente (A)\n# TYPE current_A gauge\n";
  txt += "current_A " + String(isnan(data.current)?0:data.current, 6) + "\n";
  txt += "# HELP active_power_W Potencia activa (W)\n# TYPE active_power_W gauge\n";
  txt += "active_power_W " + String(isnan(data.active_power)?0:data.active_power, 6) + "\n";
  txt += "# HELP power_factor Factor de potencia\n# TYPE power_factor gauge\n";
  txt += "power_factor " + String(isnan(data.power_factor)?0:data.power_factor, 6) + "\n";
  txt += "# HELP frequency_Hz Frecuencia (Hz)\n# TYPE frequency_Hz gauge\n";
  txt += "frequency_Hz " + String(isnan(data.frequency)?0:data.frequency, 6) + "\n";
  txt += "# HELP import_active_energy_kWh Energía importada total (kWh)\n# TYPE import_active_energy_kWh counter\n";
  txt += "import_active_energy_kWh " + String(isnan(data.import_active_energy)?0:data.import_active_energy, 6) + "\n";
  server.send(200, "text/plain; charset=utf-8", txt);
}

void handleNotFound() {
  server.send(404, "text/plain; charset=utf-8", "Not found");
}

// -------------------- Ethernet Events --------------------
void WiFiEvent(WiFiEvent_t event) {
  switch (event) {
    case ARDUINO_EVENT_ETH_START:
      ETH.setHostname("modbus-logger");
      break;
    case ARDUINO_EVENT_ETH_CONNECTED:
      break;
    case ARDUINO_EVENT_ETH_GOT_IP:
      eth_connected = true;
      eth_ip = ETH.localIP();
      break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
      eth_connected = false;
      break;
    case ARDUINO_EVENT_ETH_STOP:
      eth_connected = false;
      break;
    default:
      break;
  }
}

// -------------------- Setup --------------------
void setup() {
  pinMode(PIN_RS485_REDE, OUTPUT);
  digitalWrite(PIN_RS485_REDE, LOW); // RX por defecto
  Serial.begin(115200);
  delay(200);

  Serial.println();
  Serial.println("=== modbus-energy-logger-ethernet (ESP32-Ethernet-Kit + LAN8720 + MAX3485) ===");

  // UART2 para RS485
  RS485.begin(MODBUS_BAUD, SERIAL_8N1, PIN_RS485_RX, PIN_RS485_TX);

  // ModbusMaster: enlazar a Serial2 y configurar callbacks DE/RE
  modbus.begin(MODBUS_BAUD, RS485);
  modbus.preTransmission(preTransmission);
  modbus.postTransmission(postTransmission);
  modbus.setTimeout(MODBUS_TIMEOUT_MS);

  // Ethernet (LAN8720)
  WiFi.onEvent(WiFiEvent);
  if (!ETH.begin(ETH_PHY_ADDR, ETH_POWER_PIN, ETH_MDC_PIN, ETH_MDIO_PIN, ETH_PHY_TYPE, ETH_CLK_MODE)) {
    Serial.println("[ETH] Fallo al iniciar ETH");
  } else {
    Serial.println("[ETH] Iniciando Ethernet (DHCP)...");
  }

  // Servidor HTTP
  server.on("/", handleRoot);
  server.on("/json", handleJSON);
  server.on("/metrics", handleMetrics);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("[HTTP] Servidor iniciado en :80");
}

// -------------------- Bucle principal --------------------
uint32_t lastPoll = 0;

void loop() {
  server.handleClient();

  uint32_t now = millis();
  if (now - lastPoll >= POLL_INTERVAL_MS) {
    lastPoll = now;

    // Secuencia de lecturas Modbus (Input Registers como float 32b)
    float v;
    if (readFloatInputRegister(REG_VOLTAGE, v)) {
      data.voltage = v;
      data.ok_count++;
    } else {
      data.err_count++;
    }

    float i;
    if (readFloatInputRegister(REG_CURRENT, i)) {
      data.current = i;
      data.ok_count++;
    } else {
      data.err_count++;
    }

    float p;
    if (readFloatInputRegister(REG_ACTIVE_POWER, p)) {
      data.active_power = p;
      data.ok_count++;
    } else {
      data.err_count++;
    }

    float pf;
    if (readFloatInputRegister(REG_POWER_FACTOR, pf)) {
      data.power_factor = pf;
      data.ok_count++;
    } else {
      data.err_count++;
    }

    float f;
    if (readFloatInputRegister(REG_FREQUENCY, f)) {
      data.frequency = f;
      data.ok_count++;
    } else {
      data.err_count++;
    }

    float eimp;
    if (readFloatInputRegister(REG_IMPORT_ACTIVE_ENERGY, eimp)) {
      data.import_active_energy = eimp;
      data.ok_count++;
    } else {
      data.err_count++;
    }

    data.last_update_ms = now;

    // Trazas serie
    Serial.printf("[POLL] V=%.3f V, I=%.3f A, P=%.3f W, PF=%.3f, f=%.3f Hz, Eimp=%.3f kWh | OK=%lu ERR=%lu\n",
      data.voltage, data.current, data.active_power, data.power_factor, data.frequency, data.import_active_energy,
      (unsigned long)data.ok_count, (unsigned long)data.err_count);
  }
}

Breve explicación de partes clave:
– Inicialización ETH con LAN8720 (ETH.begin con parámetros: PHY_ADDR=0, pines MDC=23/MDIO=18, PHY=LAN8720, clock por GPIO17). Se usa DHCP. ETH.setHostname configura el hostname anunciado.
– Modbus RTU maestro sobre UART2 (GPIO4/5) y control half-duplex por GPIO15 (DE/RE). ModbusMaster usa callbacks preTransmission/postTransmission para manejar el transceptor MAX3485.
– Lecturas de registros de entrada (Input Registers) de 32 bits flotantes: se leen 2 registros consecutivos y se reempaquetan a float, con opción de swap de palabras (FLOAT_WORD_SWAP).
– WebServer nativo expone HTML, JSON y métricas de texto para facilitar integración.

Nota sobre direcciones de registros: los offsets son los usados por muchas familias SDM*. Ajuste si su medidor difiere.

Compilación, flasheo y ejecución

Preparar el proyecto

1) Crear directorio de trabajo y plantillas:

mkdir modbus-energy-logger-ethernet
cd modbus-energy-logger-ethernet
pio project init --board esp32-ethernet-kit

2) Sustituir el contenido de platformio.ini por el bloque mostrado en este documento.

3) Crear carpeta src y archivo:
– src/main.cpp con el código provisto.

Construcción

  • Instalar dependencias y compilar:
pio pkg install
pio run

Flasheo (upload)

  • Conectar el ESP32-Ethernet-Kit por micro-USB.
  • Identificar el puerto (ej.: COM5 en Windows, /dev/ttyUSB0 en Linux, /dev/tty.SLAB_USBtoUART en macOS).
  • Subir firmware:
# PlatformIO detecta el puerto automáticamente en la mayoría de casos
pio run -t upload
# Si necesitas fijar el puerto:
pio run -t upload --upload-port COM5
# o
pio run -t upload --upload-port /dev/ttyUSB0

Monitor serie

  • Abrir monitor a 115200 baudios:
pio device monitor -b 115200
  • Deberías ver mensajes tipo:
  • [ETH] Iniciando Ethernet (DHCP)…
  • evento ETH_GOT_IP con la IP asignada
  • [HTTP] Servidor iniciado en :80
  • [POLL] con valores de V, I, P, etc.

Pruebas rápidas de red

  • Descubrir la IP si no la ves en serie (desde tu router/DHCP) o usa ping:
  • Windows: ping
  • Linux/macOS: ping -c 4
  • Obtener JSON:
curl http://<IP_DEL_ESP32>/json
  • Ver HTML:
  • Navegador: http:///

Opcional: IP estática

Si tu red no usa DHCP, en setup() después de ETH.begin() puedes fijar IP:

// Sustituye por tu red
IPAddress ip(192,168,1,50);
IPAddress gw(192,168,1,1);
IPAddress mask(255,255,255,0);
IPAddress dns1(8,8,8,8), dns2(1,1,1,1);
ETH.config(ip, gw, mask, dns1, dns2);

Validación paso a paso

1) Verificación de arranque:
– En el monitor serie, confirmar:
– “Iniciando Ethernet (DHCP)…”
– Evento ETH_GOT_IP con una dirección válida (por ejemplo 192.168.1.x).
– “Servidor iniciado en :80”.

2) Link Ethernet:
– LED del puerto Ethernet activo (link/actividad).
– Desde tu PC en la misma red, ping a la IP del ESP32.
– Respuesta < 2 ms en red local típica.

3) Acceso HTTP:
– Abrir http:/// en el navegador y ver una tabla con las métricas energéticas.
– Actualizar manualmente para observar cambios (cada ~1 s se realiza un sondeo).

4) JSON y métricas:
– Ejecutar:
– curl http:///json
– curl http:///metrics
– Confirmar formato y que los valores son coherentes.

5) Validación Modbus:
– Observar en el terminal: líneas [POLL] con lecturas numéricas y contadores OK/ERR.
– Si tienes display en el medidor, comparar:
– Tensión (V)
– Corriente (A)
– Potencia activa (W)
– Frecuencia (Hz)
– PF
– Energía importada (kWh)
– Tolerancias: pequeñas diferencias por muestreo y resolución del medidor.

6) Integridad del bus RS485:
– Si el MAX3485 tiene LEDs, ver actividad en TX/RX durante los sondeos.
– A/B cableados correctamente: valores estables, ERR no aumenta continuamente.

7) Estabilidad:
– Dejar funcionando 10–15 minutos.
– OK_count sube con cada lectura; ERR_count se mantiene cerca de 0.
– Página HTML/JSON responden consistentemente.

Troubleshooting

1) No se obtiene IP (ETH_GOT_IP nunca aparece)
– Causas:
– Cable RJ45 defectuoso o puerto de switch sin enlace.
– DHCP deshabilitado o saturado.
– Pines RMII mal configurados en el código (no usar otros valores en ETH.begin).
– Soluciones:
– Cambiar cable/puerto.
– Probar con IP estática usando ETH.config().
– Verificar que ETH_MDC=23, ETH_MDIO=18, ETH_CLK_MODE=ETH_CLOCK_GPIO17_OUT, PHY_ADDR=0.

2) ETH arranca pero no hay tráfico
– Causas:
– Conmutador en VLAN aislada.
– Duplicación de IP (si IP estática).
– Soluciones:
– Validar segmentación de red.
– Cambiar IP estática o volver a DHCP.

3) Lecturas Modbus fallan (ERR_count sube rápidamente)
– Causas:
– Inversión de líneas RS485 A/B.
– Falta de terminación 120 Ω en extremos.
– No hay masa común entre sistemas.
– Baud/paridad distintos a los del medidor.
– Soluciones:
– Invertir A/B y repetir.
– Añadir/retirar terminación según topología.
– Asegurar GND común.
– Ajustar MODBUS_BAUD y formato (SERIAL_8N1; si tu medidor usa 8E1 o 8O1, cambia en RS485.begin()).

4) Valores incoherentes (p. ej., 65k V o NaN)
– Causa: Endianness de las palabras Modbus distinta.
– Solución: Cambiar FLOAT_WORD_SWAP (true/false) y volver a flashear.

5) El ESP32 no entra en modo programación (upload falla)
– Causas:
– Drivers CP210x/CH34x no instalados (Windows/macOS).
– Cable USB solo carga.
– Conflicto de puerto.
– Soluciones:
– Reinstalar drivers.
– Usar cable de datos y otro puerto USB.
– Especificar –upload-port correctamente.

6) Parpadeo de errores al iniciar (bootloop o mensajes “brownout”)
– Causas:
– Alimentación insuficiente por USB/puerto.
– Cortocircuito en conexiones al MAX3485.
– Soluciones:
– Alimentar con un puerto USB de 1 A mínimo o fuente externa 5 V.
– Revisar cableado y soldaduras.

7) Bloqueos intermitentes en Modbus con cable largo
– Causas:
– Ruido EMI, impedancia de línea, topología en estrella.
– Soluciones:
– Topología en bus, terminaciones en extremos, resistencias de polarización (bias) en A/B si el maestro no las provee, usar cable trenzado y blindado.

8) GPIO15 causa problemas al boot
– Causa:
– GPIO strap sensible; forzar niveles al arranque puede influir.
– Solución:
– Mantener GPIO15 en LOW por defecto (como hace el código). Evitar hardware externo que lo eleve en boot.

Mejoras/variantes

  • Push a InfluxDB (v2) o VictoriaMetrics:
  • Enviar con HTTP cada N segundos, formateando line protocol:
    • Ejemplo de línea: energy,host=esp32 voltage=230.1,current=1.23,power=283.0 1730800000000
  • Añadir una tarea que haga POST a http://influxdb:8086/api/v2/write?org=…&bucket=…&precision=ns con el token en cabecera.

  • MQTT sobre Ethernet:

  • Publicar en tópicos: telemetry/voltage, telemetry/current, etc., usando una librería MQTT (p. ej., PubSubClient) con ETH.

  • NTP y sellado temporal:

  • Sincronizar hora vía NTP (configTime) para registrar timestamps reales y mostrarlos en /json.

  • Configuración en runtime:

  • Exponer UI para cambiar MODBUS_SLAVE_ID, baud, intervalos y endianness desde el navegador y guardar en NVS.

  • Registro persistente:

  • Grabar CSV en SPIFFS/LittleFS con rotación diaria. Exponer descarga por HTTP.

  • Multi-esclavo:

  • Sondear varios medidores en el mismo bus ajustando MODBUS_SLAVE_ID para cada ciclo (añadir lista de esclavos).

  • Seguridad básica:

  • Autenticación HTTP simple para /json y /metrics si se expone fuera de la LAN.

  • IP estática por fallback:

  • Intentar DHCP y, si falla, aplicar una IP estática de respaldo.

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.14 y Python 3.11.6 (o compatibles) y verifiqué sus versiones.
  • [ ] Mi sistema reconoce el USB-UART (CP210x/CH34x) y sé cuál es el puerto.
  • [ ] He creado el proyecto con board = esp32-ethernet-kit y usé el platformio.ini provisto con versiones fijas.
  • [ ] He cableado el MAX3485 a los pines GPIO4 (TX), GPIO5 (RX) y GPIO15 (DE/RE), con 3V3 y GND.
  • [ ] He conectado RS485 A↔A y B↔B entre MAX3485 y medidor, con terminación 120 Ω en los extremos si procede.
  • [ ] El cable Ethernet RJ45 está conectado, y el switch/router ofrece DHCP o he configurado IP estática.
  • [ ] El firmware compila, se flashea sin errores y el monitor serie muestra ETH_GOT_IP con una IP válida.
  • [ ] Puedo abrir http:/// y ver la tabla con las métricas, y /json y /metrics responden correctamente.
  • [ ] Los valores leídos son coherentes con el display del medidor, con errores de lectura mínimos (ERR_count ~ 0).
  • [ ] Si hay incoherencias en floats, ajusté FLOAT_WORD_SWAP y confirmé la corrección.

Con todo lo anterior, has implementado un “modbus-energy-logger-ethernet” robusto y reproducible usando exactamente el “ESP32-Ethernet-Kit + LAN8720 + MAX3485 RS485” y una toolchain con versiones fijas sobre PlatformIO.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es la versión de PlatformIO Core que se debe utilizar?




Pregunta 2: ¿Qué sistema operativo no está soportado según el artículo?




Pregunta 3: ¿Cuál es la versión de Python requerida?




Pregunta 4: ¿Qué driver se recomienda para Windows?




Pregunta 5: ¿Qué librería se menciona para el proyecto?




Pregunta 6: ¿Cuál es la plataforma de hardware mencionada en el artículo?




Pregunta 7: ¿Qué comando se utiliza para verificar la versión de Python en Linux/macOS?




Pregunta 8: ¿Qué tipo de red se recomienda para las primeras pruebas?




Pregunta 9: ¿Cuál es la versión de OpenOCD-ESP32 mencionada?




Pregunta 10: ¿Qué tipo de driver USB-UART se menciona para el ESP32-Ethernet-Kit?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio