Caso práctico: Gateway TWAI/CAN↔MQTT con ESP32, SN65HVD230

Caso práctico: Gateway TWAI/CAN↔MQTT con ESP32, SN65HVD230 — hero

Objetivo y caso de uso

Qué construirás: Un gateway que conecta redes TWAI/CAN con MQTT utilizando un ESP32-WROOM-32 y un transceptor SN65HVD230.

Para qué sirve

  • Integración de vehículos con sistemas IoT mediante la transmisión de datos de sensores a través de MQTT.
  • Monitoreo en tiempo real de parámetros de vehículos como velocidad y temperatura a través de una interfaz web.
  • Control de dispositivos remotos en el vehículo mediante comandos enviados a través de MQTT.
  • Visualización de datos en tiempo real en pantallas OLED utilizando el ESP32.

Resultado esperado

  • Latencia de comunicación menor a 100 ms entre el sensor y el broker MQTT.
  • Capacidad de manejar hasta 10 paquetes/s de datos desde el bus CAN.
  • Mensajes de estado de conexión MQTT con un 99% de disponibilidad.
  • Visualización de datos en la pantalla OLED con actualización cada 2 segundos.

Público objetivo: Usuarios avanzados; Nivel: Avanzado

Arquitectura/flujo: Comunicación entre el bus TWAI/CAN y el broker MQTT a través del ESP32, con visualización en OLED.

Nivel: Avanzado

Prerrequisitos

  • Sistemas operativos soportados (elige uno):
  • Windows 10/11 (64-bit) con derechos de administrador
  • macOS 13 Ventura / macOS 14 Sonoma (Apple Silicon o Intel)
  • Ubuntu 22.04 LTS / Debian 12 (amd64 o arm64)
  • Toolchain exacta (versionado cerrado y reproducible):
  • Python 3.10.12 (Ubuntu 22.04) o Python 3.11.x en macOS/Windows
  • PlatformIO Core 6.1.12 (CLI)
  • Plataforma ESP32 para PlatformIO: espressif32@6.5.0
  • Framework Arduino-ESP32 para PlatformIO: framework-arduinoespressif32@3.20014.0 (equivale a Arduino-ESP32 2.0.14)
  • Esptool.py 4.5.1 (a través de tool-esptoolpy@1.40501.0 en PlatformIO)
  • Librerías de usuario (PlatformIO lib_deps exactas):
    • PubSubClient@2.8 (MQTT)
    • Adafruit SSD1306@2.5.7 (OLED)
    • Adafruit GFX Library@1.11.9 (OLED)
    • ArduinoJson@6.21.5 (parseo/serialización de JSON)
  • Broker MQTT:
  • Eclipse Mosquitto 2.0.18 (cliente y/o servidor local)
  • Drivers USB-UART:
  • CP210x USB to UART Bridge VCP Drivers (para la mayoría de ESP32-WROOM-32 DevKitC, chip CP2102N/CP2102)
    • Windows: Versión 10.1.10 o superior
    • macOS/Linux: normalmente no requiere instalación manual

Verificaciones rápidas:
– USB: el puerto serie aparece al conectar el “ESP32-WROOM-32 DevKitC” (ej. COMx en Windows, /dev/tty.SLAB_USBtoUART en macOS, /dev/ttyUSBx en Linux).
– PlatformIO Core: pio –version debe mostrar 6.1.12.
– Mosquitto: mosquitto -h y mosquitto_sub -h no deben dar error.

Materiales

  • ESP32-WROOM-32 DevKitC + SN65HVD230 CAN Transceiver + SSD1306 OLED (modelo exacto solicitado)
  • Cables dupont macho-hembra
  • Bus CAN funcional (al menos otro nodo CAN a 500 kbit/s) y resistencia de terminación 120 Ω en los extremos (si tu transceptor o el otro nodo no la incorporan)
  • Fuente de alimentación por USB (5 V) para el DevKitC
  • Opcional: protoboard
  • Red Wi-Fi 2.4 GHz con acceso al broker MQTT (LAN local o remoto)

Notas de alimentación:
– SN65HVD230: alimentación a 3.3 V (no 5 V). El ESP32 entrega 3.3 V en su pin 3V3.
– SSD1306 I2C: alimentación a 3.3 V (recomendado); muchos módulos soportan 3–5 V, pero con ESP32 usa 3.3 V.

Preparación y conexión

Este proyecto configura un gateway “twai-can-mqtt-gateway”:
– Lado CAN (TWAI del ESP32) con transceptor SN65HVD230 a 500 kbit/s
– Lado MQTT sobre Wi-Fi hacia un broker (ej. Mosquitto)
– OLED SSD1306 muestra estado (Wi-Fi/MQTT, frames RX/TX, estado de bus)

Conexiones recomendadas (coherentes con código, pins y librerías):

Tabla de cableado

Elemento Pin/Señal en módulo Conectar a ESP32-WROOM-32 DevKitC Notas
SN65HVD230 VCC 3V3 3.3 V únicamente
SN65HVD230 GND GND Masa común
SN65HVD230 D (TXD) GPIO 5 (TWAI_TX) Salida MCU → entrada transceptor
SN65HVD230 R (RXD) GPIO 4 (TWAI_RX) Entrada MCU ← salida transceptor
SN65HVD230 CANH CANH del bus Par trenzado con CANL
SN65HVD230 CANL CANL del bus Par trenzado con CANH
SN65HVD230 RS (mode/slope) GND RS a GND = modo normal alta velocidad
SSD1306 OLED (I2C) VCC 3V3 Alimentación 3.3 V
SSD1306 OLED (I2C) GND GND Masa común
SSD1306 OLED (I2C) SDA GPIO 21 (I2C SDA) Bus I2C
SSD1306 OLED (I2C) SCL GPIO 22 (I2C SCL) Bus I2C

Observaciones:
– Asegura que la línea CAN esté terminada con 120 Ω en ambos extremos del bus. Muchas placas SN65HVD230 incluyen un jumper/solder bridge para activar una resistencia de 120 Ω local. Actívala solo si esta placa es un extremo del bus.
– Mantén separados los cables de CAN (CANH/CANL) del resto de señales para minimizar ruido.
– El RS del SN65HVD230 controla standby y control de flanco. A GND entra en modo de velocidad alta (lo recomendado para este caso).
– No mezcles niveles de 5 V en señales lógicas con el ESP32.

Código completo

A continuación se proveen los archivos clave del proyecto PlatformIO. El código implementa:
– Inicialización de TWAI (CAN) a 500 kbit/s, filtros “accept all”
– Suscripción MQTT al tópico “twai/gw/can/tx” para inyectar frames CAN desde JSON
– Publicación de frames recibidos en “twai/gw/can/rx” en formato JSON
– Estadísticas y estado en “twai/gw/status” y “twai/gw/stats”
– OLED SSD1306 con 4 líneas: estado Wi-Fi/MQTT, estado CAN, contadores y broker/ip

platformio.ini

Crea platformio.ini en la raíz del proyecto con el siguiente contenido:

[env:esp32dev]
platform = espressif32@6.5.0
board = esp32dev
framework = arduino
platform_packages =
  framework-arduinoespressif32@3.20014.0
  tool-esptoolpy@1.40501.0
lib_deps =
  knolleary/PubSubClient@2.8
  adafruit/Adafruit SSD1306@2.5.7
  adafruit/Adafruit GFX Library@1.11.9
  bblanchon/ArduinoJson@6.21.5
upload_speed = 921600
monitor_speed = 115200
monitor_filters = time, colorize
build_flags =
  -D CONFIG_ARDUHAL_LOG_DEFAULT_LEVEL=4
  -D WIFI_SSID="\"TU_SSID\""
  -D WIFI_PASS="\"TU_PASSWORD\""
  -D MQTT_HOST="\"192.168.1.10\""
  -D MQTT_PORT=1883

Personaliza WIFI_SSID, WIFI_PASS y MQTT_HOST/MQTT_PORT a tu entorno.

src/main.cpp

Crea src/main.cpp con el contenido completo:

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <driver/twai.h>
#include <ArduinoJson.h>

// Pines y parámetros de hardware
#define TWAI_TX_GPIO GPIO_NUM_5   // SN65HVD230 D <- MCU TX
#define TWAI_RX_GPIO GPIO_NUM_4   // SN65HVD230 R -> MCU RX
#define I2C_SDA      21
#define I2C_SCL      22
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET   -1 // SSD1306 sin pin reset dedicado
#define OLED_ADDR    0x3C

// Configuración de red/MQTT (inyectadas por build_flags en platformio.ini)
#ifndef WIFI_SSID
#define WIFI_SSID "TU_SSID"
#endif
#ifndef WIFI_PASS
#define WIFI_PASS "TU_PASSWORD"
#endif
#ifndef MQTT_HOST
#define MQTT_HOST "192.168.1.10"
#endif
#ifndef MQTT_PORT
#define MQTT_PORT 1883
#endif

// Tópicos MQTT
static const char* TOPIC_RX     = "twai/gw/can/rx";
static const char* TOPIC_TX     = "twai/gw/can/tx";
static const char* TOPIC_STATUS = "twai/gw/status";
static const char* TOPIC_STATS  = "twai/gw/stats";
static const char* TOPIC_LOG    = "twai/gw/log";

WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

// Estado
volatile uint32_t can_rx_count = 0;
volatile uint32_t can_tx_count = 0;
volatile uint32_t can_rx_errors = 0;
volatile uint32_t can_tx_errors = 0;
volatile uint32_t can_bus_offs = 0;

String ipStr;
unsigned long lastStatsMs = 0;

static void oledPrintStatus(const String& line1, const String& line2, const String& line3, const String& line4) {
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println(line1);
  display.setCursor(0, 16);
  display.println(line2);
  display.setCursor(0, 32);
  display.println(line3);
  display.setCursor(0, 48);
  display.println(line4);
  display.display();
}

static void updateOLEDStatus() {
  twai_status_info_t status;
  twai_get_status_info(&status);

  char l1[32], l2[32], l3[32], l4[32];
  snprintf(l1, sizeof(l1), "WiFi:%s MQTT:%s",
           WiFi.isConnected() ? "OK" : "NO",
           mqtt.connected() ? "OK" : "NO");

  const char* st = "BUS_ON";
  if (status.state == TWAI_STATE_BUS_OFF) st = "BUS_OFF";
  else if (status.state == TWAI_STATE_ERROR_PASSIVE) st = "ERR_PASS";
  else if (status.state == TWAI_STATE_RECOVERING) st = "RECOV";

  snprintf(l2, sizeof(l2), "CAN:%s RXQ:%d TXQ:%d", st, status.msgs_to_rx, status.msgs_to_tx);
  snprintf(l3, sizeof(l3), "RX:%lu TX:%lu", (unsigned long)can_rx_count, (unsigned long)can_tx_count);
  snprintf(l4, sizeof(l4), "MQTT:%s %s", MQTT_HOST, ipStr.c_str());

  oledPrintStatus(l1, l2, l3, l4);
}

static void publishStatus() {
  StaticJsonDocument<256> doc;
  doc["wifi"]["connected"] = WiFi.isConnected();
  doc["wifi"]["ip"] = ipStr;
  doc["mqtt"]["connected"] = mqtt.connected();
  twai_status_info_t status;
  twai_get_status_info(&status);
  const char* st = "BUS_ON";
  if (status.state == TWAI_STATE_BUS_OFF) st = "BUS_OFF";
  else if (status.state == TWAI_STATE_ERROR_PASSIVE) st = "ERROR_PASSIVE";
  else if (status.state == TWAI_STATE_RECOVERING) st = "RECOVERING";
  doc["twai"]["state"] = st;
  doc["twai"]["rx_queue"] = status.msgs_to_rx;
  doc["twai"]["tx_queue"] = status.msgs_to_tx;
  doc["twai"]["rx_count"] = can_rx_count;
  doc["twai"]["tx_count"] = can_tx_count;
  doc["twai"]["rx_errs"] = can_rx_errors;
  doc["twai"]["tx_errs"] = can_tx_errors;
  doc["twai"]["bus_offs"] = can_bus_offs;

  char out[384];
  size_t n = serializeJson(doc, out, sizeof(out));
  mqtt.publish(TOPIC_STATUS, out, n);
}

static void publishStats() {
  StaticJsonDocument<192> doc;
  doc["rx"] = can_rx_count;
  doc["tx"] = can_tx_count;
  doc["host"] = MQTT_HOST;
  char out[192];
  size_t n = serializeJson(doc, out, sizeof(out));
  mqtt.publish(TOPIC_STATS, out, n);
}

static String hexBytes(const uint8_t* d, uint8_t len) {
  static const char* hex = "0123456789ABCDEF";
  String s;
  s.reserve(len * 2);
  for (uint8_t i = 0; i < len; i++) {
    s += hex[(d[i] >> 4) & 0xF];
    s += hex[d[i] & 0xF];
  }
  return s;
}

static bool parseHexData(const char* s, uint8_t* buf, uint8_t* out_len) {
  size_t L = strlen(s);
  if (L % 2 != 0) return false;
  size_t bytes = L / 2;
  if (bytes > 8) return false;
  for (size_t i = 0; i < bytes; i++) {
    char c1 = s[2 * i];
    char c2 = s[2 * i + 1];
    auto hv = [](char c) -> int {
      if (c >= '0' && c <= '9') return c - '0';
      if (c >= 'a' && c <= 'f') return c - 'a' + 10;
      if (c >= 'A' && c <= 'F') return c - 'A' + 10;
      return -1;
    };
    int h = hv(c1), l = hv(c2);
    if (h < 0 || l < 0) return false;
    buf[i] = (uint8_t)((h << 4) | l);
  }
  *out_len = (uint8_t)bytes;
  return true;
}

// Envía un frame CAN a partir de JSON recibido en TOPIC_TX
static void handleTxJson(const char* payload, size_t len) {
  StaticJsonDocument<256> doc;
  DeserializationError err = deserializeJson(doc, payload, len);
  if (err) {
    mqtt.publish(TOPIC_LOG, "JSON parse error on TX");
    return;
  }
  twai_message_t m = {};
  m.extd = doc["ext"] | false;
  m.rtr = doc["rtr"] | false;
  m.identifier = doc["id"] | 0;
  if (m.extd) {
    if (m.identifier > 0x1FFFFFFF) m.identifier = 0x1FFFFFFF;
  } else {
    if (m.identifier > 0x7FF) m.identifier = 0x7FF;
  }

  if (!m.rtr) {
    const char* dataHex = doc["data"] | "";
    uint8_t lenParsed = 0;
    uint8_t tmp[8] = {0};
    if (!parseHexData(dataHex, tmp, &lenParsed)) {
      mqtt.publish(TOPIC_LOG, "Invalid hex data");
      return;
    }
    m.data_length_code = doc["dlc"] | lenParsed;
    if (m.data_length_code > lenParsed) m.data_length_code = lenParsed;
    memcpy(m.data, tmp, m.data_length_code);
  } else {
    m.data_length_code = doc["dlc"] | 0;
  }

  esp_err_t res = twai_transmit(&m, pdMS_TO_TICKS(100));
  if (res == ESP_OK) {
    can_tx_count++;
    StaticJsonDocument<192> ack;
    ack["tx_ack"] = true;
    ack["id"] = m.identifier;
    ack["ext"] = (bool)m.extd;
    ack["dlc"] = m.data_length_code;
    char out[192];
    size_t n = serializeJson(ack, out, sizeof(out));
    mqtt.publish("twai/gw/can/tx_ack", out, n);
  } else {
    can_tx_errors++;
    mqtt.publish(TOPIC_LOG, "TX failed");
  }
}

static void mqttCallback(char* topic, byte* payload, unsigned int length) {
  if (strcmp(topic, TOPIC_TX) == 0) {
    handleTxJson((const char*)payload, (size_t)length);
  }
}

static void ensureWifi() {
  if (WiFi.isConnected()) return;
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  unsigned long start = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
    delay(250);
  }
  if (WiFi.isConnected()) {
    ipStr = WiFi.localIP().toString();
  } else {
    ipStr = "0.0.0.0";
  }
}

static void ensureMqtt() {
  if (mqtt.connected()) return;
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  String clientId = String("twai-gw-esp32-") + String((uint32_t)ESP.getEfuseMac(), HEX);
  if (mqtt.connect(clientId.c_str())) {
    mqtt.subscribe(TOPIC_TX);
    mqtt.publish(TOPIC_LOG, "MQTT connected");
  }
}

// Tarea de recepción CAN → MQTT
void can_rx_task(void* arg) {
  while (true) {
    twai_message_t m;
    esp_err_t res = twai_receive(&m, pdMS_TO_TICKS(100));
    if (res == ESP_OK) {
      can_rx_count++;
      StaticJsonDocument<256> doc;
      doc["id"] = m.identifier;
      doc["ext"] = (bool)m.extd;
      doc["rtr"] = (bool)m.rtr;
      doc["dlc"] = m.data_length_code;
      if (!m.rtr && m.data_length_code <= 8) {
        doc["data"] = hexBytes(m.data, m.data_length_code);
      }
      char out[256];
      size_t n = serializeJson(doc, out, sizeof(out));
      mqtt.publish(TOPIC_RX, out, n);
    } else if (res == ESP_ERR_TIMEOUT) {
      // Nada en cola; continúa
    } else if (res == ESP_ERR_INVALID_STATE) {
      // Driver detenido/bus_off; cuenta y espera
      can_bus_offs++;
      vTaskDelay(pdMS_TO_TICKS(100));
    } else {
      can_rx_errors++;
    }
  }
}

// Inicializa TWAI (CAN) a 500 kbit/s
static bool initTWAI() {
  twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(TWAI_TX_GPIO, TWAI_RX_GPIO, TWAI_MODE_NORMAL);
  twai_timing_config_t  t_config = TWAI_TIMING_CONFIG_500KBITS();
  twai_filter_config_t  f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();

  if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
    Serial.println("twai_driver_install failed");
    return false;
  }
  if (twai_start() != ESP_OK) {
    Serial.println("twai_start failed");
    return false;
  }
  // Opcional: fijar alertas (no indispensable en este caso)
  return true;
}

void setup() {
  Serial.begin(115200);
  delay(100);

  // I2C + OLED
  Wire.begin(I2C_SDA, I2C_SCL);
  display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("twai-can-mqtt-gateway");
  display.println("Init...");
  display.display();

  // Wi-Fi
  ensureWifi();

  // MQTT
  mqtt.setCallback(mqttCallback);
  ensureMqtt();

  // TWAI
  if (!initTWAI()) {
    oledPrintStatus("TWAI init FAILED", "", "", "");
    while (true) { delay(1000); }
  }

  // Tarea de recepción CAN
  xTaskCreatePinnedToCore(can_rx_task, "can_rx_task", 4096, nullptr, 10, nullptr, 1);

  updateOLEDStatus();
  publishStatus();
}

void loop() {
  // Mantener Wi-Fi/MQTT
  if (!WiFi.isConnected()) {
    ensureWifi();
  }
  if (WiFi.isConnected() && !mqtt.connected()) {
    ensureMqtt();
  }
  mqtt.loop();

  // Estadísticas cada 1 s
  unsigned long now = millis();
  if (now - lastStatsMs > 1000) {
    lastStatsMs = now;
    updateOLEDStatus();
    if (mqtt.connected()) {
      publishStatus();
      publishStats();
    }
  }

  delay(10);
}

Explicación de partes clave:
– TWAI a 500 kbit/s: TWAI_GENERAL_CONFIG_DEFAULT con GPIO 5 (TX) y 4 (RX). Timing con TWAI_TIMING_CONFIG_500KBITS(), filtro de aceptación total.
– MQTT PubSubClient: publica frames RX en JSON y escucha en el tópico de TX para inyectar frames.
– ArduinoJson: garantiza parseo/serialización robusto y limitado en memoria (documentos estáticos).
– OLED: muestra estado en tiempo real (Wi-Fi/MQTT, estado del controlador TWAI, contadores, host del broker e IP).
– Tareas: can_rx_task en núcleo 1 evita bloquear el loop principal que se encarga de Wi-Fi/MQTT.

Formato de JSON para transmisión (tópico twai/gw/can/tx):
– id: entero (11 bits si ext=false, 29 bits si ext=true)
– ext: booleano (false por defecto)
– rtr: booleano (false por defecto)
– dlc: 0–8 (opcional; si no se provee, se infiere de la longitud de ‘data’)
– data: string hex (hasta 16 chars para 8 bytes; p.ej. «112233AABBCCDD00»)

Ejemplo:
{«id»: 291, «ext»: false, «rtr»: false, «dlc»: 8, «data»: «1122334455667788»}

Compilación/flash/ejecución

Asumiendo PlatformIO Core 6.1.12 instalado y en PATH.

1) Crear proyecto

  • Usando CLI:
# Linux/macOS
mkdir -p ~/projects/twai-can-mqtt-gateway
cd ~/projects/twai-can-mqtt-gateway
pio project init --board esp32dev --project-option "platform=espressif32@6.5.0" --project-option "framework=arduino"

# Windows PowerShell
mkdir $env:USERPROFILE\projects\twai-can-mqtt-gateway
cd $env:USERPROFILE\projects\twai-can-mqtt-gateway
pio project init --board esp32dev --project-option "platform=espressif32@6.5.0" --project-option "framework=arduino"
  • Sustituye el contenido de platformio.ini por el dado anteriormente.
  • Crea src/main.cpp con el código completo mostrado.

2) Instalar dependencias

PlatformIO las bajará en el primer build, pero puedes forzar:

pio pkg update
pio lib -e esp32dev install "knolleary/PubSubClient@2.8"
pio lib -e esp32dev install "adafruit/Adafruit SSD1306@2.5.7"
pio lib -e esp32dev install "adafruit/Adafruit GFX Library@1.11.9"
pio lib -e esp32dev install "bblanchon/ArduinoJson@6.21.5"

3) Compilar

pio run

4) Conectar el ESP32-WROOM-32 DevKitC por USB

  • Identifica el puerto (ejemplos):
  • Windows: COM5
  • macOS: /dev/tty.SLAB_USBtoUART
  • Linux: /dev/ttyUSB0

5) Flashear

pio run -t upload

Si necesitas especificar puerto:

pio run -t upload --upload-port /dev/ttyUSB0

6) Monitor serie

pio device monitor -b 115200

Deberías ver logs de conexión Wi-Fi, conexión MQTT y arranque de TWAI.

7) Broker MQTT (Mosquitto 2.0.18)

  • Ubuntu 22.04:
sudo apt-get update
sudo apt-get install -y mosquitto=2.0.18-0mosquitto1~jammy1 mosquitto-clients=2.0.18-0mosquitto1~jammy1
sudo systemctl enable --now mosquitto
  • Verificación:
mosquitto -h | head -n 3
mosquitto_sub -h 127.0.0.1 -t "#" -v
  • Alternativa con Docker:
docker run --rm -it -p 1883:1883 eclipse-mosquitto:2.0.18

Ajusta MQTT_HOST del proyecto (platformio.ini) a la IP del broker.

Validación paso a paso

1) Verifica alimentación y cableado:
– SN65HVD230: VCC a 3V3; GND a GND; RS a GND; D a GPIO 5; R a GPIO 4.
– SSD1306: VCC a 3V3; GND a GND; SDA a GPIO 21; SCL a GPIO 22.
– CANH/CANL conectados al bus y terminación 120 Ω presente en extremos.

2) Arranque:
– En el monitor serie deben aparecer mensajes como “MQTT connected” y “twai_start OK”.
– En la OLED:
– Línea 1: WiFi:OK MQTT:OK (tras conexión)
– Línea 2: CAN:BUS_ON RXQ:0 TXQ:0 (al inicio)
– Línea 3: RX:0 TX:0 (contadores)
– Línea 4: MQTT:192.168.1.10 192.168.1.xx (IP del ESP32)

3) Suscríbete a los tópicos desde tu PC:

mosquitto_sub -h 192.168.1.10 -t 'twai/gw/#' -v

4) Inyecta un frame desde MQTT hacia CAN (si tienes otro nodo escuchando en el bus):

mosquitto_pub -h 192.168.1.10 -t 'twai/gw/can/tx' -m '{"id":291,"ext":false,"rtr":false,"dlc":8,"data":"1122334455667788"}'
  • Esperado:
  • Publicación ACK en “twai/gw/can/tx_ack” con id=291 y dlc=8
  • OLED incrementa TX
  • El otro nodo CAN debe recibir el frame (si lo monitorizas con equipo externo o un segundo nodo ESP32)

5) Ingresos desde el bus a MQTT:
– Genera frames desde otro nodo CAN a 500 kbit/s y observa en:

mosquitto_sub -h 192.168.1.10 -t 'twai/gw/can/rx' -v
  • El payload JSON debe mostrar id, ext, rtr, dlc y data en hex.

6) Estado y estadísticas:

mosquitto_sub -h 192.168.1.10 -t 'twai/gw/status' -v
mosquitto_sub -h 192.168.1.10 -t 'twai/gw/stats' -v
  • Debe mostrar bus_on o estados de error si los hubiera, colas, contadores y dirección IP.

7) Robustez:
– Apaga el broker y vuelve a encenderlo: observa reconexión automática (OLED: MQTT:NO → OK).
– Desconecta el bus CAN o genera errores: observa cambios de estado (ERROR_PASSIVE, RECOVERING, BUS_OFF) en status.

Troubleshooting

1) No aparece puerto serie:
– Windows: instala/reinstala “CP210x VCP Drivers” (Silicon Labs), usa otro cable USB de datos, prueba otro puerto.
– macOS/Linux: revisa permisos del puerto (/dev/ttyUSBx o /dev/tty.SLAB_USBtoUART). En Linux, añade tu usuario al grupo dialout: sudo usermod -aG dialout $USER y reloguea.

2) Fallo de flasheo (Timed out waiting for packet header):
– Mantén pulsado BOOT mientras reseteas (EN) el ESP32 y suelta tras iniciar upload.
– Reduce upload_speed en platformio.ini a 460800 o 115200.

3) OLED sin imagen:
– Revisa dirección I2C (0x3C es común; algunos módulos usan 0x3D). Ajusta OLED_ADDR en el código si es necesario.
– Verifica SDA/SCL (GPIO 21/22) y alimentación a 3.3 V.
– Comprueba que display.begin(…) no falle (muestra mensaje en Serial si quieres depurar).

4) MQTT no conecta:
– Verifica MQTT_HOST y MQTT_PORT. Comprueba reachability: ping MQTT_HOST y telnet MQTT_HOST 1883 (o nmap).
– Si el broker requiere autenticación/TLS, ajusta PubSubClient (no cubierto aquí). Para broker local, desactiva listeners TLS.

5) No recibo frames en twai/gw/can/rx:
– Revisa bitrate del bus (500 kbit/s en ambos nodos).
– Asegura terminaciones 120 Ω.
– Cruza CANH/CANL correctamente (no inviertas polaridad).
– Confirma que SN65HVD230 está a 3.3 V y RS a GND (standby desactivado).

6) TX falla (TX failed) o contador de errores sube:
– La línea CAN puede estar desconectada o en BUS_OFF. Verifica estado en tópico twai/gw/status.
– Comprueba que exista al menos otro nodo activo en el bus (CAN requiere confirmación de ACK por otro nodo).
– Revisa longitud del cableado y calidad del par CAN.

7) Congestión MQTT o reinicios:
– Reduce tasa de frames o filtra por IDs (modifica filtro y/o publica con QoS 0).
– Asegura buena cobertura Wi-Fi y alimentación estable (USB 5 V robusto).

8) JSON inválido o data hex incorrecta:
– Asegúrate de pares hex válidos (0–9, A–F). Longitud máxima 16 caracteres para 8 bytes.
– Si usas RTR, normalmente dlc>0 y sin campo data.

Mejoras/variantes

  • Seguridad y robustez:
  • Añadir autenticación MQTT (usuario/contraseña) y reconexión con backoff exponencial.
  • TLS con certificados (usar WiFiClientSecure y broker TLS en 8883).
  • Filtrado y enrutado:
  • Implementar listas de IDs permitidos/denegados y mapeo de tópicos por ID (p.ej. twai/gw/can/rx/0x123).
  • Añadir soporte para CAN FD (no soportado por ESP32 clásico; requeriría hardware diferente).
  • Métricas y telemetría:
  • Publicar tasa de frames (frames/s), errores acumulados por tipo, estado de alertas TWAI.
  • Persistencia/config:
  • Guardar configuración Wi-Fi/MQTT/bitrate en NVS y exponer comandos por MQTT para cambiarlos en caliente.
  • Interfaz de usuario:
  • Páginas en OLED para ver últimas tramas, ID más frecuente, tiempo desde última RX/TX.
  • Integración:
  • Contenedor Docker para broker y dashboard (Node-RED + Mosquitto + InfluxDB + Grafana).
  • Diagnóstico:
  • Añadir tópico para reiniciar controlador TWAI en BUS_OFF y medir recuperación.

Checklist de verificación

  • [ ] He instalado PlatformIO Core 6.1.12 y verifico pio –version
  • [ ] He configurado platformio.ini con:
  • [ ] platform espressif32@6.5.0
  • [ ] framework-arduinoespressif32@3.20014.0
  • [ ] tool-esptoolpy@1.40501.0
  • [ ] lib_deps con versiones: PubSubClient@2.8, Adafruit SSD1306@2.5.7, Adafruit GFX@1.11.9, ArduinoJson@6.21.5
  • [ ] He cableado exactamente: SN65HVD230 D→GPIO5, R→GPIO4, RS→GND, VCC→3V3; SSD1306 SDA→GPIO21, SCL→GPIO22, VCC→3V3
  • [ ] He verificado terminación 120 Ω en los extremos del bus CAN
  • [ ] He ajustado WIFI_SSID, WIFI_PASS, MQTT_HOST, MQTT_PORT en platformio.ini
  • [ ] El proyecto compila con pio run sin errores
  • [ ] Se programa con pio run -t upload y se abre monitor con pio device monitor -b 115200
  • [ ] En OLED veo WiFi:OK y MQTT:OK y estado CAN:BUS_ON
  • [ ] Puedo suscribirme a twai/gw/can/rx y ver frames al inyectar desde otro nodo
  • [ ] Puedo publicar en twai/gw/can/tx con JSON válido y ver ACK y aumento del contador TX
  • [ ] En caso de problemas, he revisado la sección Troubleshooting

Con esta guía avanzada has montado un gateway “twai-can-mqtt-gateway” totalmente reproducible sobre el hardware exacto ESP32-WROOM-32 DevKitC + SN65HVD230 CAN Transceiver + SSD1306 OLED, utilizando una toolchain definida y versiones concretas de framework y librerías, con rutas, comandos y validación paso a paso.

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 el sistema operativo soportado que requiere derechos de administrador?




Pregunta 2: ¿Qué versión de Python se requiere para Ubuntu 22.04?




Pregunta 3: ¿Cuál es la versión de PlatformIO Core necesaria?




Pregunta 4: ¿Qué librería se utiliza para el parseo y serialización de JSON?




Pregunta 5: ¿Qué broker MQTT se menciona en el artículo?




Pregunta 6: ¿Cuál es la versión mínima de los drivers CP210x para Windows?




Pregunta 7: ¿Qué tipo de cables se mencionan como parte de los materiales?




Pregunta 8: ¿Qué tipo de transceptor se menciona en los materiales?




Pregunta 9: ¿Qué resistencia se requiere en el bus CAN?




Pregunta 10: ¿Qué comando se debe usar para verificar la versión de PlatformIO Core?




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:
error: Contenido Protegido / Content is protected !!
Scroll to Top