A high-performance Modbus gateway system for 12 gas flow counter units, converting Modbus RTU to Modbus TCP on RP2040 with W5500 Ethernet.
- MCU: RP2040 @ 250MHz (dual-core)
- Ethernet: Wiznet W5500 controller
- RS485: Single port on UART0 with DE control and slew rate control
- SD Card: SDIO interface for data logging
- LEDs: 14 WS2812b LEDs (2 status + 12 channel indicators)
- Trigger Inputs: 12 GPIO inputs for flow counter event detection
- Non-blocking operation using queue-based architecture
- Trigger-based polling (reads all data on trigger event)
- Staggered periodic polling (one channel every 833ms, complete cycle in ~10 seconds)
- Prevents queue overflow with 12 channels (queue holds 10 requests)
- Updates temperature/pressure monitoring data
- Configurable baud rate (300-115200), parity (none/even/odd), stop bits (1 or 2), and timeout via Web UI
- Configuration changes apply immediately without system restart
- Automatic data caching with separate snapshot and current values
- Up to 4 simultaneous client connections
- Serves cached flow counter data
- Compatible with standard Modbus TCP clients
- Function codes 0x03 and 0x04 supported
Each flow counter stores two data sets:
Snapshot Data (captured on trigger events, registers 0-22):
volume(float)volume_normalised(float)flow(float)flow_normalised(float)temperature(float) - snapshot at trigger timepressure(float) - snapshot at trigger timetimestamp(uint32_t)psu_volts(float)batt_volts(float)unit_ID(10 bytes, char array)
Current Data (updated periodically, registers 30-33):
current_temperature(float) - real-time monitoringcurrent_pressure(float) - real-time monitoring
Accessing Data
- Connect via modbus TCP to port 502
- Slave ID should match target unit slave ID
- Function code 0x03 (read holding registers) supported
- Registers 0-22 for snapshot data (updated on flow trigger only)
- Registers 23-29 reserved (returns 0)
- Registers 30-33 for current data (updated via staggered polling, ~10 second cycle)
- Automatic CSV file creation per flow counter
- Filename derived from flow counter
unit_ID - Records on trigger events only
- Configurable per port
- Timestamp from flow counter (no RTC needed)
- LED 0 (System): Blinks to show system OK (orange = SD/PSU warning)
- LED 1 (RS485): Cyan = busy, Off = idle
- LED 2-13 (Channels 1-12):
- Off = Port not configured
- Purple = Configured but never connected
- Green = Data valid, communication OK
- Red = Communication error (previously connected device offline)
- Cyan = Modbus request pending
- Blue = Trigger active (overrides other states while active)
Modern dark-themed interface with:
- Dashboard: Real-time flow counter data (snapshot and current), system status, manual read buttons
- Configuration: RS485 settings (baud, parity, stop bits, timeout) and per-port flow counter config
- Network: IP configuration (DHCP/Static), hostname, Modbus TCP port
- Files: SD card file browser with system log access, view/download/delete controls
- Modbus TCP: Connection status and client list
src/
├── gateway/
│ ├── flowCounterConfig.h/cpp # Configuration management
│ └── flowCounterManager.h/cpp # ModbusRTU polling & trigger handling
├── network/
│ ├── network.h/cpp # Ethernet, web server, APIs
│ └── modbus_tcp.h/cpp # Modbus TCP server
├── storage/
│ └── sdManager.h/cpp # SD card operations
├── utils/
│ ├── logger.h/cpp # Serial/SD logging
│ ├── statusManager.h/cpp # LED management
│ └── terminalManager.h/cpp # Serial terminal
├── hardware/
│ └── pins.h # Pin definitions
├── sys_init.h/cpp # System initialization
└── main.cpp # Entry point
web/
├── index.html # Web UI structure
├── style/style.css # Dark theme styling
└── script/script.js # Dynamic functionality
lib/
└── modbus-rtu-master/ # Non-blocking Modbus RTU library
All configuration stored in LittleFS (flash memory) as JSON:
/network_config.json- Network and Modbus TCP settings/gateway_config.json- RS485 and flow counter port settings
GET /api/system/status- System health and statusGET /api/system/version- Firmware versionPOST /api/system/reboot- Reboot system
GET /api/network- Get network configurationPOST /api/network- Update network configuration
GET /api/gateway/config- Get gateway configurationPOST /api/gateway/config- Update gateway configuration (auto-reinitializes Modbus RTU)GET /api/gateway/data- Get all flow counter dataPOST /api/gateway/manual-read- Trigger manual read for specific port
GET /api/modbus-tcp/status- Get Modbus TCP statusPOST /api/modbus-tcp/config- Update Modbus TCP configuration
GET /api/sd/list?path=/- List directory contents (includes system log size)GET /api/sd/download?path=<file>- Download fileGET /api/sd/view?path=<file>- View file contents in browserDELETE /api/sd/delete?path=<file>- Delete file
Core 0 (Network & Coordination):
- Ethernet management
- Web server
- Modbus TCP server
- Network stack
Core 1 (Peripherals & Data):
- SD card operations
- LED management
- ModbusRTU master
- Flow counter trigger monitoring
- Serial terminal
# Build firmware
pio run
# Compress web files, move to /data and build filesystem image
pio run -t minify-fs
# Upload firmware
pio run -t upload
# Upload filesystem
pio run -t uploadfs- IP Mode: DHCP
- Fallback IP: 192.168.1.100
- Hostname: flow-gateway
- Modbus TCP Port: 502
- RS485 Baud: 9600
- RS485 Config: 8N1 (8 data bits, no parity, 1 stop bit)
- RS485 Timeout: 200ms
- All ports: Disabled by default
- Power on the gateway
- Connect Ethernet cable
- Find IP address (check your DHCP server or connect serial monitor)
- Open web browser to gateway IP
- Configure RS485 settings (baud rate, parity, stop bits, timeout)
- Configure each port (enable, slave ID, name, SD logging)
- Connect flow counters to RS485 daisy chain
- Connect trigger wires from flow counters to gateway trigger inputs
- Use manual read buttons to verify flow counters are responding
- Monitor dashboard for real-time snapshot and current temperature/pressure data
Each flow counter gets its own CSV file named <unit_ID>.csv:
Timestamp,Volume,Volume_Norm,Flow,Flow_Norm,Temperature,Pressure,PSU_Volts,Batt_Volts
1698765432,123.45,115.32,45.67,42.11,22.5,101.3,24.1,12.8
1698765482,128.90,120.15,46.12,42.56,22.7,101.2,24.0,12.7- Trigger inputs are active LOW (pulled high internally with INPUT_PULLUP)
- Edge-detected (not level-based) - triggers on falling edge
- When a flow counter pulls its trigger line LOW, the gateway:
- Detects the falling edge
- Queues a Modbus RTU read request for registers 0-22 (snapshot data)
- Updates the internal snapshot data cache
- Logs snapshot data to SD card if enabled
- Makes data available via Modbus TCP
- Increments trigger count for the port
- TX: GPIO 0 (UART0_TX)
- RX: GPIO 1 (UART0_RX)
- DE/RE: Controlled via
PIN_RS485_DE - Termination:
PIN_RS485_TERMset HIGH (120Ω termination active)
- Port 1-12: Connect to flow counter trigger outputs
- Each port has a dedicated GPIO input
- See
pins.hfor exact GPIO assignments
- WS2812b strip with 14 LEDs
- Data line:
PIN_LED_DAT - 5V power required
- Check cable connection
- Verify DHCP server is running
- Try setting static IP via web UI
- Verify correct slave ID configured
- Check RS485 wiring (A, B, GND)
- Verify baud rate and parity match flow counter
- Check trigger input is connected and working
- Verify SD card is formatted (FAT32 or exFAT)
- Check card is fully inserted
- Try a different SD card
- Clear browser cache
- Try accessing via IP address directly
- Re-upload filesystem:
pio run -t uploadfs
- Modbus RTU: Queue-based with one request processed at a time
- Periodic Polling: Every 10 seconds for temperature/pressure monitoring (registers 23-26)
- Modbus TCP: Can serve multiple clients simultaneously
- Update Rate: Dashboard refreshes every 2 seconds
- Trigger Check: Scanned every 10ms using edge detection
- SD Card: Logging is non-blocking
- Config Changes: RS485 settings apply immediately without restart (baud, parity, stop bits, timeout)
- LittleFS: ~1MB reserved for configuration and web files
- Stack: 8KB per core
- Heap: ~200KB available for dynamic allocation
Flow counter data is mapped to Modbus registers as follows:
Snapshot Data (updated on trigger events):
- Registers 0-1: volume (float)
- Registers 2-3: volume_normalised (float)
- Registers 4-5: flow (float)
- Registers 6-7: flow_normalised (float)
- Registers 8-9: temperature (float) - snapshot at trigger
- Registers 10-11: pressure (float) - snapshot at trigger
- Registers 12-13: timestamp (uint32_t)
- Registers 14-15: psu_volts (float)
- Registers 16-17: batt_volts (float)
- Registers 18-22: unit_ID (10 bytes)
Current Data (updated every 10 seconds):
- Registers 30-31: current_temperature (float)
- Registers 32-33: current_pressure (float)
All floats are stored as IEEE 754 single-precision (32-bit) big-endian format across 2 Modbus registers.
The RS485 interface supports Arduino SERIAL_* constants:
- 8N1 (0x413 = 1043): 8 data bits, no parity, 1 stop bit
- 8N2 (0x433 = 1075): 8 data bits, no parity, 2 stop bits
- 8E1 (0x411 = 1041): 8 data bits, even parity, 1 stop bit
- 8E2 (0x431 = 1073): 8 data bits, even parity, 2 stop bits
- 8O1 (0x412 = 1042): 8 data bits, odd parity, 1 stop bit
- 8O2 (0x432 = 1074): 8 data bits, odd parity, 2 stop bits
Copyright © 2025 Bioeconomy Science Institute - Scion Group