building an ot/ics lab with raspi 5 and esp32
IDbuilding an ot/ics lab with raspi 5 and esp32
haven’t touched microcontrollers since 2021.
now i needed a test environment for a scanner that detects ot/ics devices on a network, but real ot gear is expensive.
so i simulated them with a raspi 5 + esp32.
raspi as a multi-protocol ot server, esp32 as a real iot device with sensors and actuators.
spent about 314k idr on esp32 + components, already had the raspi.

shopping list
from a local electronics store in indonesia:
| item | qty | price |
|---|---|---|
| esp32 devkitc v4 wroom-32d + micro usb | 1 | 81k |
| w5500 ethernet lan tcp/ip module | 1 | 65k |
| relay module 5v 4ch 30a optocoupler | 1 | 110k |
| dht22 am2302 temp & humidity sensor | 1 | 22k |
| lcd 1602 i2c green backlight | 1 | 27k |
| led rgb 5mm 3-color | 5 | 3k |
| breadboard 830p | 1 | 12k |
| jumper dupont wires 15cm pack | 1 | 36k |
subtotal 359k idr, after discounts + shipping: 314k idr
raspi 5

install os, ssh in, install all services.
created 3 server scripts:
modbus tcp (port 502) - simulates a plc, holding registers + coils.
siemens s7 (port 102) - simulates siemens cpu 315-2 pn/dp using snap7.
bacnet/ip (port 47808) - simulates building automation controller using bac0.
plus mosquitto for mqtt broker (port 1883) and snmpd (port 161).
all running as systemd services.
port 22 - ssh
port 102 - siemens s7
port 161 - snmp
port 502 - modbus tcp
port 1883 - mqtt
port 5353 - mdns
port 47808 - bacnet/ip
esp32
haven’t done wiring in years, was a bit rusty looking at a breadboard again.
esp32 to w5500 via spi, dht22 for sensing, relay 4ch for actuation.
all female-to-female jumpers, breadboard only for power distribution.
esp32 devkitc is wide enough to cover all rows a-j on a breadboard. ended up not mounting it, just set it beside.
firmware
initially used the arduino ethernet library for w5500.
couldn’t run 2 servers (http + modbus) together. kept crashing.
turns out the arduino ethernet library isn’t thread-safe on esp32 (freertos). spi bus collision.
switched to eth.h (esp-idf native w5500 driver) with lwip stack. worked immediately.
features:
- modbus tcp server (port 1234)
- http status page (port 1111)
- dht22 sensor reading
- relay control via modbus or http
- mei (fc 0x2b) + report slave id (fc 0x11)
- exception responses for unsupported function codes

flashing
used arduino ide. upload speed 115200.
brltty on ubuntu hijacks cp210x usb serial:
sudo systemctl stop brltty-udev
sudo systemctl disable brltty-udev
sudo systemctl mask brltty-udev
testing
modbus to esp32
from pymodbus.client import ModbusTcpClient
client = ModbusTcpClient('192.168.0.243', port=1234)
client.connect()
result = client.read_holding_registers(address=0, count=3)
print(f'temp: {result.registers[0] / 10.0} C')
# 31.9 C - real data from dht22
s7 to raspi
import snap7
client = snap7.Client()
client.connect('192.168.0.74', 0, 1)
info = client.get_cpu_info()
# CPU 315-2 PN/DP
bacnet to raspi
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2)
packet = bytes([0x81, 0x0a, 0x00, 0x08, 0x01, 0x00, 0x10, 0x08])
sock.sendto(packet, ('192.168.0.74', 47808))
data, addr = sock.recvfrom(1024)
# 21 bytes - bacnet iam response
problems along the way
scanner couldn’t detect esp32
built a modbus scanner that enumerates devices via mei, report slave id, holding registers, coils.
initially failed on everything.
esp32 didn’t respond to unsupported function codes. no response at all, connection dropped.
scanner tried mei -> timeout -> connection reset -> every request after that was a broken pipe.
scanner fix: added reconnect logic. connection lost -> auto reconnect -> continue.
firmware fix: added exception responses (funccode | 0x80) for unsupported fc. no more hanging.
after fixing:
esp32:
method: report_slave_id
slave_id: ESP32-IoT-Controller-v1.0.0
raspi:
method: mei
manufacturer: RaspberryPi-OT-Sim
label: RPi5-PLC
firmware_version: 1.0.0
dual server crash
esp32 crashed when running http + modbus together with the arduino ethernet library.
assert failed: xQueueSemaphoreTake queue.c:1709
cause: spi bus not mutex-protected, freertos preemption caused race conditions.
fix: switched to esp-idf native eth.h driver.
wireshark
all traffic visible in wireshark.
for modbus on port 1234, need to decode as: analyze -> decode as -> tcp port 1234 -> modbus/tcp.
modbus - modbus tcp
s7comm - siemens s7
bacnet - bacnet/ip
mqtt - mqtt