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
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.



