Code Changes: From WiFi to CAN
The good news: your OpenLCB application logic doesn’t change. The event production code, configuration, and SNIP data are all transport-independent. What changes is minimal—just how the node connects to the network.
Changes in Key Code Sections
Note for Arduino IDE and Arduino Maker Workshop users: If you’re following this guide using the Arduino IDE or Arduino Maker Workshop (which uses the Arduino CLI), the class names in these examples may differ. These tools can access newer Arduino Core versions that use different class names and namespacing. See the next section (“The Esp32HardwareTwai Class: Arduino Cores and the Name Change”) for details on what adjustments are needed for your environment.
Includes and Definitions (main.cpp)
#include <Arduino.h>
-#include <WiFi.h>
#include <SPIFFS.h>
#include <OpenMRNLite.h>
-#include "utils/GcTcpHub.hxx"
+#include "utils/GpioInitializer.hxx"
#include "config.h"
-// WiFi credentials - CHANGE THESE to match your network
-const char* ssid = "YourWiFiSSID";
-const char* password = "YourWiFiPassword";
// OpenLCB Node ID - must be unique on your network
static constexpr uint64_t NODE_ID = 0x050201020200ULL;
// Event IDs that will be alternated
static const uint64_t EVENT_ID_0 = 0x0502010202000000ULL;
static const uint64_t EVENT_ID_1 = 0x0502010202000001ULL;
+/// This is the ESP32 pin connected to the SN65HVD23x/MCP2551 R (RX) pin.
+constexpr gpio_num_t CAN_RX_PIN = GPIO_NUM_4;
+
+/// This is the ESP32 pin connected to the SN65HVD23x/MCP2551 D (TX) pin.
+constexpr gpio_num_t CAN_TX_PIN = GPIO_NUM_5;
+
// Create the OpenMRN stack object
OpenMRN openmrn(NODE_ID);
-// TCP Hub for JMRI connectivity
-GcTcpHub* tcp_hub = nullptr;
+Esp32HardwareTwai can_driver(CAN_RX_PIN, CAN_TX_PIN);
The setup() Function
void setup() {
init_serial();
init_filesystem();
- init_network();
+ can_driver.hw_init();
init_openlcb_stack();
- init_network();
+ openmrn.add_can_port_select("/dev/twai/twai0");
Serial.println("OpenLCB node initialization complete!");
Serial.printf("Entering run mode - will alternate events every %lu ms\n", event_interval);
// Record start time for event production
last_event_time = millis();
}
Understanding the Changes
1. Removed WiFi Includes and Code
#include <WiFi.h>— no longer needed for network connectivity#include "utils/GcTcpHub.hxx"— no longer needed (no TCP Hub)- WiFi SSID/password variables — replaced by CAN GPIO pin definitions
- TCP Hub global variable — removed entirely
1.5 Removed Functions Entirely
Two complete functions are deleted:
init_network() — This entire function is removed:
void init_network() {
Serial.printf("\nConnecting to WiFi SSID: %s\n", ssid);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWiFi connected!");
Serial.printf("IP Address: %s\n", WiFi.localIP().toString().c_str());
}
init_tcp_hub() — This entire function is removed:
void init_tcp_hub() {
Serial.println("Starting TCP Hub on port 12021...");
tcp_hub = new GcTcpHub(openmrn.stack()->can_hub(), 12021);
Serial.println("TCP Hub listening. JMRI can connect to this device on port 12021");
}
Both are replaced by a single line in setup():
openmrn.add_can_port_select("/dev/twai/twai0");
2. Added CAN Transceiver Definition
The Esp32HardwareTwai class represents your CAN transceiver on the breadboard. It takes two GPIO pins (RX and TX) as parameters:
- GPIO_NUM_4 (RX) — receives CAN messages
- GPIO_NUM_5 (TX) — transmits CAN messages
3. Initialization Order
The key difference: can_driver.hw_init() initializes the ESP32’s TWAI hardware and sets up GPIO4/GPIO5 for CAN signaling. This must happen before starting the OpenMRN stack.
4. Port Registration
Instead of creating a TCP Hub for JMRI connectivity, you register the TWAI device as a CAN port:
add_can_port_select("/dev/twai/twai0")— registers the TWAI device"/dev/twai/twai0"— the filesystem device name for TWAI hardwareselectmeans the driver uses Unixselect()for efficient I/O
The OpenMRN stack reads/writes CAN messages through this device, routing them to and from your OpenLCB application.
Complete Updated setup() Function
Here’s the full, updated setup function:
void setup() {
init_serial();
init_filesystem();
// Initialize CAN transceiver hardware
can_driver.hw_init();
// Initialize OpenMRN stack and configuration
init_openlcb_stack();
// Register the TWAI CAN device as the network port
openmrn.add_can_port_select("/dev/twai/twai0");
Serial.println("OpenLCB node initialization complete!");
Serial.printf("Entering run mode - will alternate events every %lu ms\n", event_interval);
// Record start time for event production
last_event_time = millis();
}
What Stays the Same
The following parts of your code are unchanged:
- Event production logic in
loop()— still produces EVENT_ID_0 and EVENT_ID_1 - Configuration structure (config.h) — blink interval is now configurable via JMRI
- SNIP data (node name, description) — initialized on factory reset
- FactoryResetHelper — now also initializes default blink_interval (1000 ms)
- apply_configuration() — now reads the blink_interval from config when JMRI updates settings
- openmrn.loop() — still required in the main loop
Key Additions: Configuration Management
This CAN version adds runtime configuration for the blink interval:
- Global variable
event_interval— cached value read from configuration - FactoryResetHelper.apply_configuration() — now reads
cfg.seg().blink_interval()when configuration changes - FactoryResetHelper.factory_reset() — initializes default blink_interval to 1000 ms on first boot
- loop() function — uses the configurable
event_intervalinstead of a constant
This means users can change the blink speed through JMRI without recompiling the firmware.
The OpenLCB protocol stack is completely independent of the physical transport layer. This is excellent software architecture—change WiFi to CAN, and the node behaves identically.
Compiling and Uploading
The code is ready to compile and upload using the same method as Chapter 3: click Upload and Monitor in the PlatformIO project tasks.
When the upload completes and the device boots, you’ll see the same startup messages as before:
Node ID: 0x050201020200
Event 0: 0x0502010202000000
Event 1: 0x0502010202000001
Initializing SPIFFS...
SPIFFS initialized successfully
ESP-TWAI: Configuring TWAI (TX:5, RX:4, EXT-CLK:-1, BUS-CTRL:-1)
Creating CDI configuration descriptor...
ESP-TWAI: Starting TWAI watchdog and reporting task
[CDI] Checking /spiffs/cdi.xml...
[CDI] File /spiffs/cdi.xml appears up-to-date (len 1172 vs 1172)
[CDI] Registering CDI with stack...
Initializing OpenLCB configuration...
Starting OpenLCB stack...
Starting executor thread...
OpenLCB stack initialized successfully
ESP-TWAI: Starting TWAI driver on:twai0 mCode:3ffb41cc (non-blocking) fd:0
onfiguration updated: blink_interval = 2000 ms
OpenLCB node initialization complete!
Entering run mode - will alternate events every 2000 ms
Allocating new alias C41 for node 050201020200
Produced event: 0x0502010202000001 (state: 1)
Produced event: 0x0502010202000000 (state: 0)...
The node is now producing events on the CAN bus instead of the WiFi TCP Hub.
Next: Understand the Esp32HardwareTwai class and why it’s named that.