Modbus Router
The Modbus Router (jproc-modbusrouter) is a configurable Modbus gateway that sits between Modbus masters (e.g. SCADA systems) and slave devices (inverters, meters, sensors). It provides protocol translation, unit ID mapping, failover routing, request caching, and peer-to-peer forwarding between JBoxes via RemoteChannel2 (RC2).
Table of Contents
- Modbus Router
- Table of Contents
- Architecture Overview
- How It Works
- Configuration
- Configuration Examples
- Request Routing
- Protocol Translation
- Request Caching
- Diagnostics \& Monitoring
- Web UI
- Modbus Exception Codes
- Related Components
- Troubleshooting
Architecture Overview
┌─────────────────────────────────────────────────┐
│ Modbus Router │
│ │
┌──────────┐ │ ┌──────────────┐ ┌───────────────────┐ │ ┌──────────────┐
│ SCADA │────▶│ │ ModbusServer │────▶│ Routing Engine │─────│────▶│ Inverter │
│ (Master) │◀────│ │ (TCP :502) │◀────│ │◀────│────▶│ (Slave) │
└──────────┘ │ └──────────────┘ │ • Unit ID map │ │ └──────────────┘
│ │ • Protocol xlat │ │
┌──────────┐ │ ┌──────────────┐ │ • Caching │ │ ┌──────────────┐
│ Monitor │────▶│ │ ModbusServer │────▶│ • Failover │─────│────▶│ Meter │
│ (Master) │◀────│ │ (RTU serial) │◀────│ • Diagnostics │◀────│────▶│ (Slave) │
└──────────┘ │ └──────────────┘ │ │ │ └──────────────┘
│ │ │ │
│ │ │─────│───▶ ┌──────────────┐
│ │ │◀────│──── │ Remote JBox │
│ │ │ │ │ (via RC2) │
│ └───────────────────┘ │ └──────────────┘
└─────────────────────────────────────────────────┘
The router is composed of these core classes:
| Class | Role |
|---|---|
JProcModbusRouterProcess |
JProc entry point — loads config, manages lifecycle, handles RC2 messages |
ModbusRouter |
Core routing engine — routes requests, manages connections, translates protocols |
ModbusServer |
Listens for incoming master connections (TCP or RTU) |
ModbusSlaveSession |
RAII session handler for a single request/response to a slave device |
SlaveData |
Per-slave statistics, health tracking, and request caching |
How It Works
- Masters connect to
ModbusServerinstances (TCP or RTU) configured as ingress points. - An incoming Modbus request contains a logical unit ID that the router uses to look up the destination.
- The router translates the logical unit ID to a physical unit ID and forwards the request to the correct slave connection.
- If the slave is behind a remote JBox, the request is forwarded over RC2 (WebSocket).
- The response is translated back (physical → logical unit ID, protocol conversion) and returned to the master.
- If the primary route fails, the router automatically tries the failover unit ID if one is configured.
Configuration
Process Configuration
The process config is stored in jproc-modbusrouter.json:
{
"id": "jproc-modbusrouter",
"router_config_filename": "routing.json",
"default_interface": "br0",
"rc2_message_timeout_ms": 1000
}
| Field | Type | Description |
|---|---|---|
router_config_filename |
string | Filename of the routing configuration (loaded from config-user directory) |
default_interface |
string | Default network interface for TCP server bindings (e.g. br0, eth0) |
rc2_message_timeout_ms |
int | Timeout in milliseconds for RC2 peer requests |
Routing Configuration
The routing configuration (routing.json) defines all masters, slaves, and their relationships:
{
"enabled": true,
"diagnostics_enabled": true,
"mappings": [ ... ],
"logical_id_failover_mappings": [ ... ]
}
| Field | Type | Description |
|---|---|---|
enabled |
bool | Master switch — enables or disables the entire router |
diagnostics_enabled |
bool | Enables detailed per-slave diagnostics and stats collection |
mappings |
array | List of master/slave endpoint definitions |
logical_id_failover_mappings |
array | Failover chains between logical unit IDs |
Connection Types
Each mapping has a connection object. Three connection types are supported:
TCP
{
"connection": {
"host": "192.168.1.100",
"port": 502,
"interface": "br0"
}
}
| Field | Type | Description |
|---|---|---|
host |
string | IPv4 address of the device |
port |
int | Modbus TCP port (typically 502) |
interface |
string | (Optional, masters only) Network interface to bind to. An IP alias is added to this interface for the server to listen on. |
Serial (RTU)
{
"connection": {
"dev": "/dev/ttyAMA0",
"baudrate": 9600,
"parity": "N",
"databits": 8,
"stopbits": 1
}
}
| Field | Type | Description |
|---|---|---|
dev |
string | Serial device path (e.g. /dev/ttyAMA0, /dev/ttyUSB0) |
baudrate |
int | Baud rate (9600, 19200, 38400, 57600, 115200) |
parity |
string | Parity: "N" (none), "E" (even), "O" (odd) |
databits |
int | Data bits (7 or 8) |
stopbits |
int | Stop bits (1 or 2) |
JBox Peer (RC2 Fingerprint)
{
"connection": {
"fingerprint": "AB:CD:EF:12:34:56:78:90"
}
}
| Field | Type | Description |
|---|---|---|
fingerprint |
string | Hardware fingerprint of the remote JBox. Requests are forwarded over the RC2 WebSocket relay. |
Unit ID Mappings
Each non-master mapping includes a unit_ids array. Unit IDs can be specified in simple or detailed mode:
Simple Mode
Just the logical ID — physical ID defaults to the same value, default timeout and throttle:
{
"unit_ids": [1, 2, 3]
}
Detailed Mode
Full control over logical/physical mapping and per-slave tuning:
{
"unit_ids": [
{
"logical": 1,
"physical": 7,
"timeout": 1000,
"min_request_interval": 500
},
{
"logical": 2,
"physical": 8,
"timeout": 2000,
"min_request_interval": 0
}
]
}
| Field | Type | Default | Description |
|---|---|---|---|
logical |
int | — | Unit ID exposed to masters. Must be unique across all mappings. |
physical |
int | same as logical | Unit ID used when communicating with the actual slave device. |
timeout |
int | 500 ms | Response timeout in milliseconds for this slave. |
min_request_interval |
int | 500 ms | Minimum interval between requests to this slave. Enables request caching when > 0. Set to 0 to disable caching. |
Note: Logical unit IDs must be unique across the entire routing configuration. The Web UI will warn you about duplicates.
Failover Mappings
Failover mappings define automatic rerouting when a primary slave becomes unreachable:
{
"logical_id_failover_mappings": [
{ "primary": 1, "failover": 10 },
{ "primary": 2, "failover": 11 }
]
}
| Field | Type | Description |
|---|---|---|
primary |
int | Logical unit ID of the primary slave |
failover |
int | Logical unit ID of the fallback slave. Must also be defined in a mapping. |
When a request to the primary slave fails (connection error, timeout), the router automatically retries the request using the failover unit ID. Both the primary and failover must be valid logical IDs defined in the mappings.
Configuration Examples
Basic TCP-to-TCP Routing
A SCADA system connects to the router on 10.0.0.50:502. The router forwards requests to two inverters on the local network:
{
"enabled": true,
"diagnostics_enabled": true,
"mappings": [
{
"master": true,
"connection": {
"host": "10.0.0.50",
"port": 502,
"interface": "br0"
}
},
{
"connection": {
"host": "192.168.1.100",
"port": 502
},
"unit_ids": [
{ "logical": 1, "physical": 1, "timeout": 1000, "min_request_interval": 0 },
{ "logical": 2, "physical": 2, "timeout": 1000, "min_request_interval": 0 }
]
}
],
"logical_id_failover_mappings": []
}
TCP Master with RTU Slaves
A TCP master routes to devices on a serial bus:
{
"enabled": true,
"diagnostics_enabled": false,
"mappings": [
{
"master": true,
"connection": {
"host": "10.0.0.50",
"port": 502,
"interface": "eth0"
}
},
{
"connection": {
"dev": "/dev/ttyUSB0",
"baudrate": 9600,
"parity": "N",
"databits": 8,
"stopbits": 1
},
"unit_ids": [1, 2, 3, 4, 5]
}
],
"logical_id_failover_mappings": []
}
Peer Routing with Failover
Route to a device on a remote JBox, with a local fallback:
{
"enabled": true,
"diagnostics_enabled": true,
"mappings": [
{
"master": true,
"connection": {
"host": "10.0.0.50",
"port": 502,
"interface": "br0"
}
},
{
"connection": {
"fingerprint": "30b8ef6a54626a65a37f"
},
"unit_ids": [
{ "logical": 1, "physical": 1, "timeout": 2000, "min_request_interval": 500 }
]
},
{
"connection": {
"host": "192.168.1.200",
"port": 502
},
"unit_ids": [
{ "logical": 10, "physical": 1, "timeout": 1000, "min_request_interval": 0 }
]
}
],
"logical_id_failover_mappings": [
{ "primary": 1, "failover": 10 }
]
}
In this example, requests to logical unit 1 go to the remote JBox. If that fails, the router retries with logical unit 10 which is a local device at 192.168.1.200:502.
Request Routing
Local Path (Direct)
For TCP and RTU slave connections, the router communicates directly:
Master Request
│
▼
┌─────────────────────────┐
│ 1. Look up logical ID │
│ 2. Map to physical ID │
│ 3. Translate protocol │
│ (TCP ↔ RTU) │
│ 4. Check cache │
└──────────┬──────────────┘
│
┌──────▼───────┐
│ SlaveSession │ ← RAII mutex lock (one request at a time per slave)
│ connect() │
│ send() │
│ receive() │
└──────┬───────┘
│
┌──────▼─────────────────┐
│ 5. Translate response │
│ (physical → logical)│
│ 6. Update cache │
│ 7. Update diagnostics │
└────────────────────────┘
The ModbusSlaveSession uses RAII to ensure:
- A mutex is held for the duration of the request (prevents concurrent access to the same connection)
- The connection is cleanly disconnected when the session ends
Peer Path (RC2)
For slave connections identified by a JBox fingerprint:
Master Request
│
▼
┌───────────────────────────────┐
│ 1. Serialize frame to hex │
│ 2. Convert to TCP format │
│ 3. Call RC2 request callback │
└──────────┬────────────────────┘
│
▼
┌───────────────────────────────┐
│ RC2 WebSocket Relay │
│ Local JBox → Relay → Remote │
└──────────┬────────────────────┘
│
▼
┌───────────────────────────────┐
│ Remote JBox receives frame │
│ → Routes to local slave │
│ → Returns response │
└──────────┬────────────────────┘
│
┌──────▼───────────────┐
│ 4. Deserialize hex │
│ 5. Translate response│
│ 6. Return to master │
└──────────────────────┘
RC2 return codes:
| Code | Name | Description |
|---|---|---|
| 0 | SUCCESS |
Request completed successfully |
| -1 | ERROR |
General error |
| -2 | TIMEOUT |
RC2 request timed out (rc2_message_timeout_ms) |
| -3 | INVALID_PEER |
Remote JBox not found or not connected |
| -4 | UNKNOWN_ERROR |
Unclassified error |
| -5 | INVALID_UNIT_ID |
Requested unit ID not on remote JBox |
| -6 | FAILED_SLAVE_CONNECTION |
Remote JBox couldn't connect to its slave |
Failover Path
When a request to the primary unit ID fails:
Primary Request Failed
│
▼
┌────────────────────────────────────┐
│ Look up failover unit ID │
│ (from logical_id_failover_mappings)│
└──────────┬─────────────────────────┘
│
┌──────▼───────────────┐
│ Retry with failover │
│ unit ID using the │
│ same request path │
└──────┬───────────────┘
│
┌──────▼───────────────┐
│ Update diagnostics │
│ • failover_count++ │
│ or failed_failover++ │
└──────────────────────┘
Failover is triggered by: - TCP/RTU connection failure - Slave response timeout - RC2 errors (timeout, invalid peer, failed slave connection)
Protocol Translation
The router automatically converts between Modbus TCP and RTU frame formats. The translation is handled by prepareRequestFrame() and prepareResponseFrame():
Request Translation (Master → Slave)
| Master Format | Slave Format | Action |
|---|---|---|
| TCP | TCP | Extract Transaction ID (TID) from MBAP header, replace unit ID with physical |
| TCP | RTU | Strip MBAP header, extract TID, convert to RTU format |
| RTU | TCP | Add MBAP header with default TID, set physical unit ID |
| RTU | RTU | Strip CRC (libmodbus handles CRC), set physical unit ID |
Response Translation (Slave → Master)
| Slave Format | Master Format | Action |
|---|---|---|
| TCP | TCP | Restore original TID, set logical unit ID |
| TCP | RTU | Strip MBAP header, set logical unit ID |
| RTU | TCP | Add MBAP header with original TID, set logical unit ID |
| RTU | RTU | Strip CRC, set logical unit ID |
Modbus Frame Formats
TCP (MBAP Header + PDU):
[Transaction ID: 2 bytes][Protocol ID: 2 bytes][Length: 2 bytes][Unit ID: 1 byte][Function Code: 1 byte][Data: N bytes]
RTU (Unit ID + PDU + CRC):
[Unit ID: 1 byte][Function Code: 1 byte][Data: N bytes][CRC: 2 bytes]
Request Caching
When min_request_interval is set to a value > 0 for a slave, the router caches responses to avoid excessive polling:
- Cache key: The request frame (converted to RTU format and hex-encoded), keyed per master.
- Cache hit: If the same request was sent within the
min_request_interval, the cached response is returned immediately. For TCP masters, the Transaction ID is updated in the cached response. - Write bypass: Write function codes (FC 05, 06, 0F, 10, 21, 22, 23) are never cached.
- Cache reset: The entire cache is automatically cleared every 1 hour.
This is useful for scenarios where multiple masters poll the same slave for the same registers — the router serves cached data instead of flooding the slave.
Diagnostics & Monitoring
When diagnostics_enabled is true, the router collects detailed per-slave statistics.
Per-Slave Metrics
| Metric | Description |
|---|---|
request_count |
Total requests sent to this slave |
response_count |
Total successful responses received |
error_count |
Total errors (connection, protocol, exception) |
timeout_count |
Total response timeouts |
consecutive_errors |
Current streak of consecutive errors (resets on success) |
failover_count |
Times failover was triggered for this slave |
failed_failover_count |
Times failover itself also failed |
invalid_request_count |
Requests that couldn't be processed |
cache_hit_count |
Responses served from cache |
cache_miss_count |
Requests that went to the actual slave |
requests_per_second |
Calculated over 30-second intervals |
avg_ms / min_ms / max_ms |
Response latency statistics (rolling window of ~20 samples) |
last_request_ms |
Timestamp of last request |
last_success_ms |
Timestamp of last successful response |
last_error_ms |
Timestamp of last error |
exception_codes |
Histogram of Modbus exception codes received |
recent_frames |
Last 15 request/response pairs with timestamps and hex dumps |
healthy |
Boolean — false when consecutive errors exceed threshold |
A background stats thread dumps all metrics to modbus_router_stats.json every 30 seconds for persistence.
Diagnostics JSON
The full diagnostics payload (accessible via MsgGetRoutingDiagnostics or the /get_routing_diagnostics API endpoint):
{
"connections": [
{
"name": "192.168.1.100:502",
"type": "direct",
"connection_stats": {
"total_requests": 1500,
"total_responses": 1498,
"connection_drop_count": 1
},
"slaves": [
{
"logical_unit_id": 1,
"physical_unit_id": 7,
"failover_unit_id": 10,
"healthy": true,
"counters": {
"request_count": 750,
"response_count": 748,
"error_count": 2,
"timeout_count": 1,
"consecutive_errors": 0,
"failover_count": 1,
"failed_failover_count": 0,
"invalid_request_count": 0,
"cache_hit_count": 200,
"cache_miss_count": 550
},
"timestamps": {
"last_request_ms": 1712345678000,
"last_success_ms": 1712345678000,
"last_error_ms": 1712345600000
},
"latency": {
"avg_ms": 45,
"max_ms": 120,
"min_ms": 12
},
"requests_per_second": 2.5,
"exception_codes": {
"2": 1,
"11": 1
},
"recent_frames": [
{
"timestamp_ms": 1712345678000,
"request": "0100000001",
"response": "01010100",
"success": true
}
]
}
]
}
],
"masters": [
{
"name": "10.0.0.50:502 (br0)",
"type": "tcp",
"running": true
}
],
"totals": {
"request_count": 1500,
"response_count": 1498,
"error_count": 2,
"timeout_count": 1,
"cache_hit_count": 200,
"cache_miss_count": 550
}
}
Web UI
The Modbus Router is configured and monitored through the JBox Manager web interface under System Info → Options → Modbus Routing Config.
Topology View
The topology view is a 3-column layout:
┌────────────────────┬──────────────────────┬─────────────────────┐
│ Masters (Ingress) │ Local Router │ Slaves (Egress) │
│ │ │ │
│ ┌──────────────┐ │ ┌────────────────┐ │ ┌───────────────┐ │
│ │ TCP │ │ │ Enabled: ✓ │ │ │ TCP │ │
│ │ 10.0.0.50:502│──│──│ Diagnostics: ✓ │──│──│ 192.168.1.100 │ │
│ │ (br0) │ │ │ Mappings: 3 │ │ │ Unit IDs: 1,2 │ │
│ └──────────────┘ │ │ Failovers: 1 │ │ └───────────────┘ │
│ │ └────────────────┘ │ │
│ ┌──────────────┐ │ │ ┌───────────────┐ │
│ │ RTU │ │ Tabs: │ │ JBox Peer │ │
│ │ /dev/ttyAMA0 │──│──[Config|Diagnostics]│──│ AB:CD:EF:... │ │
│ │ @ 9600 │ │ │ │ Unit IDs: 3 │ │
│ └──────────────┘ │ │ └───────────────┘ │
└────────────────────┴──────────────────────┴─────────────────────┘
Features: - Toggle router enabled/disabled - Toggle diagnostics enabled/disabled - Add, edit, and delete mappings via modal - Add and remove failover mappings - Visual indicators for connection type (TCP/RTU/Peer icons) - Duplicate logical ID warnings - Input validation (IPv4 format, required fields) - Tabs switch between Configuration and Diagnostics views
Editing a Mapping
The mapping editor modal has two tabs:
Connection Tab: - Connection type selector: TCP / Serial (RTU) / JBox Peer - TCP: Host (IPv4), Port, Interface (master only) - Serial: COM port dropdown, Baud rate, Parity, Data bits, Stop bits - Peer: Target JBox fingerprint
Unit IDs Tab (slaves only): - Detailed mode: Logical ID, Physical ID, Timeout (ms), Throttle Interval (ms) per row - Simple mode: Logical ID only (physical = logical, default timeout/throttle) - Bulk add: Enter a range (e.g. 1–248) to add multiple unit IDs at once - Add/remove individual unit IDs
Diagnostics Dashboard
Auto-refreshes every 10 seconds. Shows:
Header Stats Bar: - Total Requests, Responses, Errors, Timeouts - Cache Hit Rate percentage
Masters Section: - List of all master connections with type (TCP/RTU/Peer) and running status
Per-Connection Cards (expandable): - Connection name and type - Total requests, responses, connection drops
Per-Slave Cards (within each connection): - Health indicator (green ● or red ●) - Unit IDs: Logical, Physical, Failover - Requests/second - All counters (requests, responses, errors, timeouts, failovers, cache stats) - Latency bar: avg/min/max with color coding: - Green: < 50ms - Yellow: 50–200ms - Red: > 200ms - Cache efficiency progress bar - Timestamps: Last request, last success, last error (relative time) - Modbus exception code histogram with tooltips for code names - Recent frames table: timestamp, request hex, response hex, success/fail
Modbus Exception Codes
When a slave returns an exception response, the router logs the exception code. Common codes:
| Code | Name | Description |
|---|---|---|
| 1 | Illegal Function | Function code not supported by the slave |
| 2 | Illegal Data Address | Register address out of range |
| 3 | Illegal Data Value | Value in the request data field is invalid |
| 4 | Server Failure | Unrecoverable error on the slave |
| 5 | Acknowledge | Request accepted but processing takes time |
| 6 | Server Busy | Slave is busy, retry later |
| 10 | Gateway Path Unavailable | Gateway couldn't reach the target |
| 11 | Gateway Target Failed to Respond | No response from target behind gateway |
Related Components
| Component | Role |
|---|---|
| Modbus Library | Low-level ModbusConnection, RegisterDecoder, RegisterEncoder |
| Modbus Simulator | jproc-modbussim — simulates slave devices for testing |
| RemoteChannel2 | WebSocket relay protocol used for peer-to-peer JBox communication |
| JProcDeviceBridge | Bridges and proxies Modbus devices across JBoxes via data model |
| JProcDataBroker | Polls devices and publishes data to the data model |
Troubleshooting
Router not starting
- Check that
routing.jsonhas"enabled": true - Check that
jproc-modbusrouteris listed inservices.json - Verify the process is running: look for
jproc-modbusrouterin the supervisor status
Master not receiving responses
- Enable diagnostics and check the Diagnostics Dashboard for errors/timeouts
- Verify the slave device is reachable (use the Modbus Test tool in the Web UI)
- Check that logical unit IDs match what the master is requesting
- Look at
recent_framesin diagnostics to see actual request/response hex
High timeout counts
- Increase the
timeoutvalue in the unit ID mapping - Check physical connectivity to the slave device
- For serial (RTU) connections, verify baud rate, parity, and other serial settings match the device
- For peer (RC2) connections, check that the remote JBox is online and the fingerprint is correct
Cache not working
- Ensure
min_request_intervalis > 0 for the slave's unit ID mapping - Write requests (FC 05, 06, 0F, 10) are never cached by design
- Cache resets automatically every 1 hour
Failover not triggering
- Verify both the primary and failover logical unit IDs exist in the mappings
- Check
logical_id_failover_mappingshas the correct primary → failover pair - Use diagnostics to confirm
failover_countis incrementing
Duplicate logical ID warnings in the UI
- Each logical unit ID must be unique across all slave mappings
- Remove or reassign duplicate IDs in the Configuration topology view
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /get_routing_config |
Returns current routing.json |
| POST | /set_routing_config |
Updates routing config (triggers hot-reload) |
| GET | /get_routing_diagnostics |
Returns full diagnostics JSON |
This article was last modified: 8 Apr 2026, 6:36 p.m.