Examples and Tutorials ⤴

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


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

  1. Masters connect to ModbusServer instances (TCP or RTU) configured as ingress points.
  2. An incoming Modbus request contains a logical unit ID that the router uses to look up the destination.
  3. The router translates the logical unit ID to a physical unit ID and forwards the request to the correct slave connection.
  4. If the slave is behind a remote JBox, the request is forwarded over RC2 (WebSocket).
  5. The response is translated back (physical → logical unit ID, protocol conversion) and returned to the master.
  6. 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

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.json has "enabled": true
  • Check that jproc-modbusrouter is listed in services.json
  • Verify the process is running: look for jproc-modbusrouter in 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_frames in diagnostics to see actual request/response hex

High timeout counts

  • Increase the timeout value 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_interval is > 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_mappings has the correct primary → failover pair
  • Use diagnostics to confirm failover_count is 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.