[**Examples and Tutorials ⤴**](./ExamplesTutorials.md)
# 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](#modbus-router)
- [Table of Contents](#table-of-contents)
- [Architecture Overview](#architecture-overview)
- [How It Works](#how-it-works)
- [Configuration](#configuration)
- [Process Configuration](#process-configuration)
- [Routing Configuration](#routing-configuration)
- [Connection Types](#connection-types)
- [TCP](#tcp)
- [Serial (RTU)](#serial-rtu)
- [JBox Peer (RC2 Fingerprint)](#jbox-peer-rc2-fingerprint)
- [Unit ID Mappings](#unit-id-mappings)
- [Simple Mode](#simple-mode)
- [Detailed Mode](#detailed-mode)
- [Failover Mappings](#failover-mappings)
- [Configuration Examples](#configuration-examples)
- [Basic TCP-to-TCP Routing](#basic-tcp-to-tcp-routing)
- [TCP Master with RTU Slaves](#tcp-master-with-rtu-slaves)
- [Peer Routing with Failover](#peer-routing-with-failover)
- [Request Routing](#request-routing)
- [Local Path (Direct)](#local-path-direct)
- [Peer Path (RC2)](#peer-path-rc2)
- [Failover Path](#failover-path)
- [Protocol Translation](#protocol-translation)
- [Request Translation (Master → Slave)](#request-translation-master--slave)
- [Response Translation (Slave → Master)](#response-translation-slave--master)
- [Modbus Frame Formats](#modbus-frame-formats)
- [Request Caching](#request-caching)
- [Diagnostics \& Monitoring](#diagnostics--monitoring)
- [Per-Slave Metrics](#per-slave-metrics)
- [Diagnostics JSON](#diagnostics-json)
- [Web UI](#web-ui)
- [Topology View](#topology-view)
- [Editing a Mapping](#editing-a-mapping)
- [Diagnostics Dashboard](#diagnostics-dashboard)
- [Modbus Exception Codes](#modbus-exception-codes)
- [Related Components](#related-components)
- [Troubleshooting](#troubleshooting)
- [Router not starting](#router-not-starting)
- [Master not receiving responses](#master-not-receiving-responses)
- [High timeout counts](#high-timeout-counts)
- [Cache not working](#cache-not-working)
- [Failover not triggering](#failover-not-triggering)
- [Duplicate logical ID warnings in the UI](#duplicate-logical-id-warnings-in-the-ui)
- [API Endpoints](#api-endpoints)
---
## 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`:
```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:
```json
{
"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
```json
{
"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)
```json
{
"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)
```json
{
"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:
```json
{
"unit_ids": [1, 2, 3]
}
```
#### Detailed Mode
Full control over logical/physical mapping and per-slave tuning:
```json
{
"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:
```json
{
"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:
```json
{
"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:
```json
{
"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:
```json
{
"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):
```json
{
"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](./modbus.md) | Low-level `ModbusConnection`, `RegisterDecoder`, `RegisterEncoder` |
| [Modbus Simulator](./modbussim.md) | `jproc-modbussim` — simulates slave devices for testing |
| [RemoteChannel2](./rc2.md) | 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 |
Feedback
This article was last modified: 8 Apr 2026, 6:36 p.m.