> For the complete documentation index, see [llms.txt](https://docs.roboflow.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.roboflow.com/deployment/self-hosted/enterprise/deployment-manager/services/plc-relay.md).

# PLC Relay

PLC Relay is an edge container service that provides an HTTP API for reading and writing PLC tags. You configure it through the Deployment Manager UI by selecting a protocol, entering connection details, and defining tags.

{% hint style="info" %}
PLC Relay is available exclusively for Enterprise customers. [Contact the Roboflow sales team](https://roboflow.com/sales) to learn more.
{% endhint %}

## Supported Protocols

When adding or editing a PLC Relay service, you select one of three protocols. Each protocol has its own connection settings and tag format.

<table data-header-hidden data-search="false"><thead><tr><th></th><th></th><th></th><th></th></tr></thead><tbody><tr><td>Protocol</td><td><code>PLC_DRIVER</code></td><td>PLCs</td><td>Default Port</td></tr><tr><td>Allen-Bradley (EtherNet/IP)</td><td><code>allen_bradley</code></td><td>CompactLogix, ControlLogix, Micro800</td><td>44818</td></tr><tr><td>Modbus TCP</td><td><code>modbus</code></td><td>Any Modbus TCP device</td><td>502</td></tr><tr><td>Siemens S7</td><td><code>siemens_s7</code></td><td>S7-300, S7-400, S7-1200, S7-1500</td><td>102</td></tr></tbody></table>

{% hint style="warning" %}
Switching protocols clears all configured tags because tag address formats are not interchangeable between protocols. The UI will prompt for confirmation before applying the change.
{% endhint %}

## Connection Settings

### PLC Address

The address format depends on the selected protocol:

* **Allen-Bradley:** IP or hostname, optionally followed by `/slot` (e.g. `192.168.1.100/0`) or a full CIP routing path.
* **Modbus TCP:** IP or hostname, with optional `:port` (e.g. `192.168.1.100:502`). Also requires a Unit ID (0-255) and Word Order (big or little) for 32-bit values.
* **Siemens S7:** IP or hostname, with optional `:port` (e.g. `192.168.1.100:102`). Also requires Rack (0-7) and Slot (0-31).

### Simulation Mode

When enabled, PLC Relay uses an in-memory simulator instead of connecting to a real PLC. All API operations work normally, but values are stored in memory. This is useful for testing without hardware.

## Tag Configuration

Tags define the PLC data points accessible through the API. Each tag has a name, data type, writable flag, and optional description.

### Data Types

| Type   | Description           | Range                           |
| ------ | --------------------- | ------------------------------- |
| `BOOL` | Boolean               | `true` / `false`                |
| `INT`  | 16-bit signed integer | -32,768 to 32,767               |
| `DINT` | 32-bit signed integer | -2,147,483,648 to 2,147,483,647 |
| `REAL` | 32-bit floating point | IEEE 754                        |

### Tag Name Formats

{% tabs %}
{% tab title="Allen-Bradley" %}
Tag names match the PLC program and are case-sensitive.

| Style          | Example                       |
| -------------- | ----------------------------- |
| Simple         | `TagName`                     |
| Program-scoped | `Program:MainProgram.TagName` |
| Array element  | `TagName[0]`                  |
| UDT member     | `MyUDT.Member`                |
| {% endtab %}   |                               |

{% tab title="Modbus TCP" %}
Format: `{area}:{address}` where address is a non-negative integer.

| Area         | Type(s)         | Access    | Example       |
| ------------ | --------------- | --------- | ------------- |
| `coil`       | BOOL            | Writable  | `coil:0`      |
| `discrete`   | BOOL            | Read-only | `discrete:5`  |
| `holding`    | INT, DINT, REAL | Writable  | `holding:100` |
| `input`      | INT, DINT, REAL | Read-only | `input:200`   |
| {% endtab %} |                 |           |               |

{% tab title="Siemens S7" %}
Data Block format: `DB{n}.DB[XWD]{byte}[.{bit}]`

Area format: `[MIQEA][WD]?{byte}[.{bit}]`

| Address         | Type               | Description                     |
| --------------- | ------------------ | ------------------------------- |
| `DB1.DBX0.0`    | BOOL               | Bit 0 of byte 0 in Data Block 1 |
| `DB1.DBW0`      | INT                | 16-bit word in DB1              |
| `DB1.DBD0`      | DINT or REAL       | 32-bit double-word in DB1       |
| `M0.0`          | BOOL               | Merker bit                      |
| `I0.0` / `Q0.0` | BOOL               | Process input/output bit        |
| `MW0` / `MD0`   | INT / DINT or REAL | Merker word / double-word       |

For S7-1200/1500: enable PUT/GET in TIA Portal and disable optimized block access on accessed DBs.
{% endtab %}
{% endtabs %}

## Web Dashboard

PLC Relay includes a built-in web dashboard for monitoring tag values in real time. Once the service is running, access it at `http://<device-ip>:8007`.

The dashboard also hosts interactive Swagger documentation at `/docs` and a visual config builder at `/static/config-builder.html`.

## HTTP API

The API reads and writes the configured tags over HTTP, so a pipeline can exchange PLC data without speaking Allen-Bradley EtherNet/IP, Modbus TCP, or Siemens S7 directly. Base URL is `http://<device-ip>:8007`, and the API is unauthenticated. See [Services](/deployment/self-hosted/enterprise/deployment-manager/services.md#using-the-apis) for the rules shared by all on-device service APIs.

Tags come from the `PLC_TAGS` environment variable on the service and cannot be created or changed through the API. Only values can be written, and only for tags configured as writable.

### Read Before You Trust the Status Code

A request that reaches the service but fails at the PLC returns `200`. Check the body:

* `/read` returns `value: null` with `error` populated.
* `/write` returns `success: false` with `error` populated.
* `/healthz` returns `plc_connected: false`.

Genuine `4xx` responses mean the request itself was wrong: `404` for a tag that is not configured, `403` for a tag configured as read-only, `400` for an empty batch or a duplicate tag name in a write batch, `422` for a body or query parameter that fails validation.

### Health and Validation

`/healthz` also reports the active driver, whether the relay is in `live` or `simulation` mode, and the latest tag validation summary. Validation compares each configured tag against the PLC and reports it as `ok`, `not_found`, `type_mismatch`, or `not_validated`.

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/healthz" method="get" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

Re-run validation after a PLC program change or a reconnection:

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/validate" method="post" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

### Tag Definitions

Tag names are PLC addresses in the format for the active driver, described in [Tag Name Formats](#tag-name-formats).

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/schema" method="get" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

### Read and Write Values

```bash
curl "http://<device-ip>:8007/read?tag=Station1.CycleCount"

curl -X POST http://<device-ip>:8007/write \
  -H "Content-Type: application/json" \
  -d '{"name": "Station1.CycleCount", "value": 42}'
```

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/all\_tags" method="get" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/read" method="get" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/write" method="post" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

### Batch Operations

Both batch endpoints validate as a unit before anything runs, so an unknown tag rejects the whole request rather than returning partial results.

`/write_batch` additionally rejects a batch that names the same tag twice, since keeping only the last value for a repeated name would silently drop the earlier writes. `/read_batch` accepts duplicates and returns one result per entry, in the order you sent them.

PLC failures that happen after validation are reported per entry in `write_batch`'s `results`, with `success_count` and `error_count` summarizing the batch.

```bash
curl -X POST http://<device-ip>:8007/read_batch \
  -H "Content-Type: application/json" \
  -d '{"tags": ["Station1.PartPresent", "Station1.CycleCount"]}'
```

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/read\_batch" method="post" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

{% openapi src="/files/sLbfwc4nzTThFtccS6aC" path="/write\_batch" method="post" %}
[edge-plc-relay.yaml](https://1583372177-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoksNmwps1HmYr8TqHDnX%2Fuploads%2Fgit-blob-bc81a2258bd5eefa9b64b301eac0797c0f01e796%2Fedge-plc-relay.yaml?alt=media)
{% endopenapi %}

## CLI

The `plc-cli` tool gives an interactive terminal interface for reading and writing tags. Use it for debugging and quick operations when the web dashboard is not reachable. It is a local client running inside the container against the same HTTP API the dashboard uses.

```bash
docker exec -it plc-relay plc-cli
```

<table data-search="false"><thead><tr><th>Key</th><th>Action</th></tr></thead><tbody><tr><td><code>R</code></td><td>Read all tag values</td></tr><tr><td><code>T</code></td><td>Read a single tag, selected from a list</td></tr><tr><td><code>W</code></td><td>Write a tag, selected from the writable tags</td></tr><tr><td><code>S</code></td><td>Show the tag schema</td></tr><tr><td><code>H</code></td><td>Show detailed health status</td></tr><tr><td><code>V</code></td><td>Run tag validation against the PLC</td></tr><tr><td><code>Enter</code></td><td>Refresh the display</td></tr><tr><td><code>Q</code></td><td>Quit</td></tr></tbody></table>

## Environment Variables

The Configure modal writes these for you. Edit them directly only when a deployment is managed by hand. See [Update Device Configuration](/deployment/self-hosted/enterprise/deployment-manager/making-changes/update-device-configuration.md).

<table data-search="false"><thead><tr><th>Variable</th><th>Default</th><th>Description</th></tr></thead><tbody><tr><td><code>PLC_DRIVER</code></td><td>none</td><td><code>allen_bradley</code>, <code>modbus</code>, or <code>siemens_s7</code></td></tr><tr><td><code>PLC_IP</code></td><td>none</td><td>PLC address in the format for the selected driver</td></tr><tr><td><code>PLC_TAGS</code></td><td>none</td><td>Tag definitions as JSON. The config builder at <code>/static/config-builder.html</code> generates this</td></tr><tr><td><code>SIMULATION_MODE</code></td><td>off</td><td>Use the in-memory simulator instead of a real PLC</td></tr><tr><td><code>LOG_LEVEL</code></td><td><code>INFO</code></td><td>Logging verbosity: <code>DEBUG</code>, <code>INFO</code>, <code>WARNING</code>, or <code>ERROR</code></td></tr></tbody></table>

Driver-specific settings:

<table data-search="false"><thead><tr><th>Driver</th><th>Variable</th><th>Range</th><th>Default</th></tr></thead><tbody><tr><td>Modbus</td><td><code>MODBUS_UNIT_ID</code></td><td>0 to 255</td><td><code>1</code></td></tr><tr><td>Modbus</td><td><code>MODBUS_WORD_ORDER</code></td><td><code>big</code> or <code>little</code></td><td><code>big</code></td></tr><tr><td>Siemens S7</td><td><code>S7_RACK</code></td><td>0 to 7</td><td><code>0</code></td></tr><tr><td>Siemens S7</td><td><code>S7_SLOT</code></td><td>0 to 31</td><td><code>1</code></td></tr></tbody></table>

## Connection Monitoring

The device page shows a live PLC Relay status card with the current connection state, the active protocol, and the latest tag values. When the relay cannot reach the PLC, the card displays an unreachable banner.

To be notified when a relay loses connectivity, add a "PLC Disconnected" alert from the device's "Device Alerts" tab. See [Set up Device Alerts](/deployment/self-hosted/enterprise/deployment-manager/setting-up/set-up-device-alerts.md).

## Troubleshooting

| Symptom                              | Fix                                                                                                                    |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| "PLC not connected" (Allen-Bradley)  | Verify PLC address format (IP/Slot), check port 44818 is reachable                                                     |
| "PLC not connected" (Modbus)         | Check IP/port (default 502) and verify Unit ID matches the device                                                      |
| "PLC not connected" (Siemens S7)     | Check IP/port (default 102), rack, and slot values; for S7-1200/1500 enable PUT/GET and disable optimized block access |
| "Function refused" (Siemens S7)      | PUT/GET disabled in TIA Portal, or optimized block access enabled on the target DB                                     |
| REAL value reads as garbage (Modbus) | Try the opposite Word Order (big vs. little)                                                                           |
| Validation shows NOT\_FOUND          | Check the PLC program for the exact tag name (case-sensitive)                                                          |
