Layer: L7 Application ยท Build flags: PC_ENABLE_MODBUS
What this example teaches
Modbus TCP is the lingua franca of industrial automation. This serves a small Modbus data model (coils, discrete inputs, holding and input registers) on TCP/502 so a SCADA system, PLC, or a tool like mbpoll can read and write it. It also shows the second listener pattern: a non-HTTP protocol bound to its own port on the same server.
A second protocol listener on its own port:
pc_modbus_server_init();
pc_modbus_set_holding_reg(0, 0x1234);
pc_modbus_set_input_reg(0, 0);
pc_modbus_on_write(on_write);
server.begin();
@ PROTO_MODBUS
Modbus TCP slave (Modbus Application Protocol).
Who owns what. The application writes input registers / discrete inputs (which are read-only to the client) to publish sensor state, reads holding registers / coils the client has written, and is notified of client writes through pc_modbus_on_write(fc, start, count). The loop publishes a live uptime value into an input register the client can poll.
Security note. Modbus has no authentication or encryption: run it only on a trusted control network, fronted by the per-IP accept throttle (L4-Transport/PerIpThrottle).
Build and run
pio ci --board=esp32dev --project-option="framework=arduino" \
--project-option="build_flags=-DPC_ENABLE_MODBUS=1" \
--lib="." examples/L7-Application/ModbusTcp/ModbusTcp.ino
mbpoll -m tcp -t 4:hex -r 1 -c 2 <ip> # read holding regs 0..1
mbpoll -m tcp -t 0 -r 1 1 <ip> # write coil 0 = 1
Annotated source
The complete sketch (ModbusTcp.ino), reproduced verbatim with added explanatory comments:
#define PC_ENABLE_MODBUS 1
static const char *SSID = "YOUR_SSID";
static const char *PASSWORD = "YOUR_PASSWORD";
static void on_write(uint8_t fc, uint16_t start, uint16_t count)
{
Serial.printf("client write: fc=0x%02X start=%u count=%u\n", fc, start, count);
}
void setup()
{
Serial.print("Connecting to WiFi");
{
delay(250);
Serial.print('.');
}
Serial.printf("IP: %u.%u.%u.%u\n", (unsigned)(ip & 0xFF), (unsigned)((ip >> 8) & 0xFF),
(unsigned)((ip >> 16) & 0xFF), (unsigned)((ip >> 24) & 0xFF));
pc_modbus_server_init();
pc_modbus_set_holding_reg(0, 0x1234);
pc_modbus_set_input_reg(0, 0);
pc_modbus_on_write(on_write);
Serial.println("Modbus TCP slave on :502");
}
void loop()
{
static uint32_t last = 0;
if (millis() - last >= 1000)
{
last = millis();
pc_modbus_set_input_reg(0, (uint16_t)(millis() / 1000));
}
}
Single-port HTTP server with deterministic, zero-allocation execution.
int32_t listen(uint16_t port, ConnProto proto=ConnProto::PROTO_HTTP)
Register a port to listen on when begin() is called.
int32_t begin(const WebServerConfig *cfg=nullptr)
Initialize all connection slots and open all registered listeners.
void handle()
Drive the server - call every Arduino loop() iteration.
Zero-heap Modbus TCP slave/server (Modbus Application Protocol v1.1b3).
bool init_wifi_physical(const char *, const char *)
Connect to a WiFi access point.
uint32_t pc_net_egress_ip(void)
IPv4 (network byte order) of the current egress interface, or 0 if none.
bool wifi_ready()
True if the WiFi station link is up (associated + an IP is assigned).
Layer 1 (Physical) - link bring-up and live egress-interface reporting.
Layer 7 (Application) - public HTTP routing API.