The Nominal Instrumentation Library is in alpha testing. To request access, open the
Support Portal.
EtherNet/IP
EtherNet/IP config files declare an Allen-Bradley PLC endpoint, an optional local backplane route, and the scalar tags Nominal reads or writes. EtherNetIPDevice exposes those tags by local alias from Python and can publish boolean and numeric tag values to Nominal Core or Nominal Connect.
The EtherNet/IP client lives under instro.unstable.ethernetip and requires the optional native package instro-ethernetip-python. Install it with instro-unstable[ethernetip]; it is not part of the stable instro[all] install path.
Install
Install the unstable package with the EtherNet/IP extra:
uv add "instro-unstable[ethernetip]"
or with pip:
pip install "instro-unstable[ethernetip]"
Quickstart
This example opens a CompactLogix connection, polls scalar tags, and publishes numeric samples to Nominal Core.
import time
from instro.lib.publishers import NominalCorePublisher
from instro.unstable.ethernetip import EtherNetIPDevice
RID = "<dataset_rid>" # Nominal Core dataset RID.
# autostart opens the session and starts polling.
plc = EtherNetIPDevice(config="compactlogix.json", autostart=True)
# Publish boolean and numeric tag samples to Nominal Core.
plc.add_publisher(NominalCorePublisher(dataset_rid=RID))
# Let the background daemon collect one polling window.
time.sleep(10)
plc.close()
Create compactlogix.json with the PLC connection and tag definitions:{
"version": 1,
"protocol": "ethernetip",
"device": {
"name": "line_plc",
"description": "CompactLogix PLC for line telemetry",
"manufacturer": "Allen-Bradley",
"model": "CompactLogix 5332E 1769-L32E"
},
"connection": {
"host": "192.168.1.10",
"port": 44818,
"route_path": {
"hops": [
{"type": "backplane", "slot": 0}
]
}
},
"timing": {
"poll_interval": 1.0
},
"tags": [
{
"alias": "motor_running",
"tag_name": "MotorRunning",
"data_type": "bool",
"poll": true
},
{
"alias": "line_speed",
"tag_name": "LineSpeed",
"data_type": "real",
"poll": true
},
{
"alias": "recipe_name",
"tag_name": "RecipeName",
"data_type": "string",
"poll": false
}
]
}
Current support
EtherNet/IP support is intentionally narrow. It targets PLC tags whose names and scalar data types are known before runtime.
| Area | Supported today |
|---|
| PLC family | Tested on Allen-Bradley CompactLogix 5332E 1769-L32E |
| Transport | EtherNet/IP explicit messaging over TCP |
| Route paths | Direct connection, or local backplane slot hops only |
| Background polling | Automatic batched reads for all poll: true tags |
| Streaming to Nominal Core or Nominal Connect | Boolean and numeric scalar tag values |
| Manual tag operations | Single-tag and batched scalar reads, plus writes, through the native session API |
| Unsigned integer validation | usint, uint, udint, and ulint are implemented, but not validated |
| Tag discovery | Not supported |
| UDTs | Not supported in the config-driven API |
| Arrays | Not supported in the config-driven API |
Other Allen-Bradley Logix-family PLCs may work through the same underlying protocol. Current hardware testing covers only CompactLogix 5332E 1769-L32E.
When to use each feature
A connection, scalar tag definitions, and autostart=True cover the common path. Use the fields and APIs below for specific EtherNet/IP requirements.
| Requirement | Use |
|---|
| Stream live scalar PLC data to Nominal Core or Nominal Connect | timing.poll_interval + tags with poll: true |
| Poll configured tags automatically | timing.poll_interval + tags with poll: true |
| Reject out-of-range setpoints before PLC writes | write_min / write_max on numeric tags |
| Reach a PLC CPU behind a local chassis/backplane | connection.route_path.hops with type: "backplane" |
| Read or write a string tag manually | EtherNetIpSession.read_tag() / EtherNetIpSession.write_tag() |
| Read several PLC tags in one native request | EtherNetIpSession.read_tags([...]) |
Key concepts
Lifecycle
The EtherNet/IP workflow has six moves:
- Define PLC tags in a JSON config file or an
EtherNetIPConfig.
- Instantiate
EtherNetIPDevice(config) with an optional connection override.
- Call
open() to establish the native EtherNet/IP session.
- Call
start() to begin background polling when timing is configured.
- Read or write tags by alias with type validation.
- Call
close() to close the session and stop background polling.
Pass autostart=True to combine steps 3 and 4. When a timing section is configured, autostart also starts the background polling daemon.
Opening the session and validating the config do not probe the PLC tag map. Tag existence and actual PLC type are only validated when PLC I/O touches the tag: either when the background polling task reads poll: true tags, or when the user explicitly calls read_tag(), read_tags(), or write_tag().
Example timing section:
{
"timing": {
"poll_interval": 1.0
}
}
Supported data types
The config-driven API supports scalar PLC tag types. The data_type field is required for every tag so reads can validate that the PLC returned the expected kind before publishing. If the PLC returns a different kind than the configured data_type, EtherNetIPDevice.read_tag() raises TypeError.
| Data type | Python payload | Streaming to Nominal Core or Nominal Connect | Notes |
|---|
bool | bool read as 0 or 1 in measurements | Yes | Boolean reads are published as numeric samples. |
sint | int | Yes | 8-bit signed integer. |
int | int | Yes | 16-bit signed integer. |
dint | int | Yes | 32-bit signed integer. |
lint | int | Yes | 64-bit signed integer. |
usint | int | Yes | 8-bit unsigned integer. Implemented, but not validated. |
uint | int | Yes | 16-bit unsigned integer. Implemented, but not validated. |
udint | int | Yes | 32-bit unsigned integer. Implemented, but not validated. |
ulint | int | Yes | 64-bit unsigned integer. Implemented, but not validated. |
real | float | Yes | 32-bit floating point. |
lreal | float | Yes | 64-bit floating point. |
string | str | No | Allen-Bradley Logix STRING (LEN as DINT, DATA as SINT[]). Manual reads and writes only. |
The config-driven EtherNet/IP API supports scalar tags only. Structured values cannot be declared as a tag data_type; if a configured scalar tag actually returns a structured value, it is treated as an expected-versus-actual PLC kind mismatch. EtherNetIPDevice.read_tag() raises TypeError.
Strings
String support is for Allen-Bradley Logix STRING tags encoded as LEN (DINT) plus DATA (SINT[]). String measurements are not supported by Nominal Core or Nominal Connect. Manual string tag reads and writes are still supported, but not through the background polling thread.
In config files, string tags must set poll: false:
{
"alias": "recipe_name",
"tag_name": "RecipeName",
"data_type": "string",
"poll": false
}
Use the private native session for Python-side string reads or writes:
from instro.unstable._ethernetip import EtherNetIpSession, PlcValue
with EtherNetIpSession("192.168.1.10:44818", route_path_slots=[0]) as session:
current = session.read_tag("RecipeName")
print(current.value)
session.write_tag("RecipeName", PlcValue.string("startup"))
The public config-driven EtherNetIPDevice.write_tag() method can write string command tags when poll is disabled:
plc.write_tag("recipe_name", "startup")
Batched reads
Use the private native session to read several PLC tags in one request:
from instro.unstable._ethernetip import EtherNetIpBatchError, EtherNetIpSession
with EtherNetIpSession("192.168.1.10:44818", route_path_slots=[0]) as session:
for name, result in session.read_tags(["MotorRunning", "LineSpeed"]):
if isinstance(result, EtherNetIpBatchError):
print(f"{name} failed: {result}")
continue
print(name, result.kind, result.value)
read_tags() preserves input order. The call raises EtherNetIpError only when the whole batch cannot be dispatched or parsed. Individual tag failures are returned in the result list as typed EtherNetIpBatchError instances such as TagNotFoundError, DataTypeMismatchError, or CipError, so successful tags in the same batch remain available.
Background polling
With timing configured and polling started, EtherNetIPDevice reads every tag with poll: true in one batched native request at the configured interval.
If one tag in the batch fails, that tag is skipped for the current measurement and the other successful tag values are still published.
Poll only streamable scalar tags:
bool
- signed and unsigned integer types
real
lreal
String tags are not polled because string measurements cannot be published to Nominal Core or Nominal Connect. UDTs and arrays are not polled because they are currently unsupported by the underlying EtherNet/IP library.
Route paths
Route paths are limited to local backplane slot hops:
{
"connection": {
"host": "192.168.1.10",
"port": 44818,
"route_path": {
"hops": [
{"type": "backplane", "slot": 0}
]
}
}
}
The config schema allows multiple local backplane hops:
{
"route_path": {
"hops": [
{"type": "backplane", "slot": 2},
{"type": "backplane", "slot": 0}
]
}
}
Current testing has covered one backplane hop.
Network hops are not supported. A route path cannot hop through Ethernet to another PLC, remote chassis, or IP address. The only accepted hop type is backplane, and CIP port 1 is implied by the slot value.
Tag discovery
Tag discovery is not supported. Every tag must be declared explicitly in the tags array with:
- a local
alias
- the PLC
tag_name
- the expected scalar
data_type
- optional
poll, write_min, and write_max
If the PLC tag’s actual type differs from the configured data_type, an EtherNetIPDevice.read_tag() call raises TypeError.
Write limits
Set write_min and write_max to reject unsafe writes before they reach the PLC:
{
"alias": "speed_setpoint",
"tag_name": "SpeedSetpoint",
"data_type": "real",
"write_min": 0.0,
"write_max": 2500.0
}
plc.write_tag("speed_setpoint", 1200.0) # OK
plc.write_tag("speed_setpoint", 9999.0) # Raises ValueError
EtherNet/IP checks write limits against the value passed by the caller before sending the write to the PLC.
Configuration
Every EtherNet/IP config file must include:
version: identifies the config schema version.
protocol: must be "ethernetip" so the wrong protocol config fails clearly.
device: metadata used for channel naming.
tags: explicit tag definitions.
Full configuration example
{
"version": 1,
"protocol": "ethernetip",
"device": {
"name": "line_plc",
"description": "CompactLogix PLC for line telemetry",
"manufacturer": "Allen-Bradley",
"model": "CompactLogix 5332E 1769-L32E"
},
"connection": {
"host": "192.168.1.10",
"port": 44818,
"route_path": {
"hops": [
{"type": "backplane", "slot": 0}
]
}
},
"timing": {
"poll_interval": 1.0
},
"tags": [
{
"alias": "ready",
"tag_name": "Ready",
"data_type": "bool"
},
{
"alias": "line_speed",
"tag_name": "LineSpeed",
"data_type": "real",
"write_min": 0.0,
"write_max": 2500.0
},
{
"alias": "recipe_name",
"tag_name": "RecipeName",
"data_type": "string",
"poll": false
}
]
}
Device section
The device section describes the physical PLC. EtherNetIPDevice uses the name field as a prefix for published channel names.
| Field | Required | Description |
|---|
name | Yes | Device name, used as channel name prefix |
description | No | Human-readable description |
manufacturer | No | Device manufacturer |
model | No | Device model number |
Connection section
| Field | Required | Default | Description |
|---|
host | Yes | - | PLC IP address or hostname |
port | No | 44818 | EtherNet/IP TCP port |
route_path | No | - | Optional local backplane route path |
Route path
| Field | Required | Default | Description |
|---|
hops | No | [] | Ordered list of local backplane hops |
Each hop must be:
| Field | Required | Description |
|---|
type | Yes | Must be "backplane" |
slot | Yes | Backplane slot number, 0-255 |
Tag definitions
Each tag entry defines one aliasable PLC tag:
| Field | Required | Default | Description |
|---|
alias | Yes | - | Unique local alias used in read_tag() and write_tag() |
tag_name | Yes | - | PLC tag name |
data_type | Yes | - | Expected scalar PLC data type |
description | No | - | Optional human-readable description |
poll | No | true | Include this tag in background polling |
write_min | No | - | Minimum allowed write value for numeric tags |
write_max | No | - | Maximum allowed write value for numeric tags |
Timing section
| Field | Required | Default | Description |
|---|
poll_interval | Yes | - | Polling interval in seconds (0.01-10.0) |
When the timing section is present, EtherNetIPDevice reads tags with poll: true in a batched background request at the specified interval. Successful values are published and buffered for retrieval.
Validation rules
EtherNetIPDevice validates configuration at load time:
protocol must be "ethernetip".
- Tag aliases must be unique.
- Every tag must declare
data_type.
data_type must be one of the supported scalar types.
- Only numeric tags can use
write_min and write_max.
write_min must be less than or equal to write_max.
- Integer write limits must fit in the configured PLC integer type.
- String tags must set
poll: false.
- Route path hops must be local backplane hops with slots from 0 to 255.
Load-time validation is local to the config. It does not verify that PLC tags exist or that their actual PLC types match data_type; those checks happen only when the background polling task runs or when the user explicitly initiates a read_tag(), read_tags(), or write_tag() operation.
Create an EtherNetIPDevice instance
JSON config
Connection override
Python config
from instro.unstable.ethernetip import EtherNetIPDevice
# Uses the connection section from compactlogix.json.
plc = EtherNetIPDevice(config="compactlogix.json")
plc.open()
measurement = plc.read_tag("line_speed")
plc.write_tag("line_speed", 1200.0)
plc.close()
from instro.unstable.ethernetip import EtherNetIPDevice
plc = EtherNetIPDevice(
config="compactlogix.json",
# Overrides any connection in the config; the config can omit connection.
connection={
"host": "192.168.1.10",
"port": 44818,
# Local backplane hop to the PLC CPU slot.
"route_path": {"hops": [{"type": "backplane", "slot": 0}]},
},
)
plc.open()
from instro.lib.types import DeviceInfo
from instro.unstable.ethernetip import (
EtherNetIPConfig,
EtherNetIPConnectionInfo,
EtherNetIPDevice,
TagDef,
TimingConfig,
)
config = EtherNetIPConfig(
device=DeviceInfo(name="line_plc"),
# Required when autostart should begin background polling.
timing=TimingConfig(poll_interval=1.0),
tags=[
TagDef(alias="ready", tag_name="Ready", data_type="bool"),
TagDef(alias="line_speed", tag_name="LineSpeed", data_type="real"),
],
)
# Keep environment-specific connection details separate from the tag map.
connection = EtherNetIPConnectionInfo(
host="192.168.1.10",
# Local backplane hop to the PLC CPU slot.
route_path={"hops": [{"type": "backplane", "slot": 0}]},
)
# Opens the session and starts polling immediately because timing is configured.
plc = EtherNetIPDevice(config, connection=connection, autostart=True)
Read and write
Returned Measurement and Command objects use channel names in the pattern {device_name}.{tag_alias}. Write commands append .cmd to the channel name.
# The config examples above name the device "line_plc".
measurement = plc.read_tag("line_speed")
print(measurement.channel_data["line_plc.line_speed"])
command = plc.write_tag("line_speed", 1200.0)
print(command.channel_data["line_plc.line_speed.cmd"])
Background polling
Use autostart=True to open the session and begin background polling immediately:
from instro.lib.publishers import NominalCorePublisher
from instro.unstable.ethernetip import EtherNetIPDevice
plc = EtherNetIPDevice(config="compactlogix.json", autostart=True)
plc.add_publisher(NominalCorePublisher(dataset_rid="<dataset_rid>"))
recent_speed = plc.get_channel("line_plc.line_speed", length=100)
plc.close()
Start explicitly when the session should open before polling begins:
plc = EtherNetIPDevice(config="compactlogix.json")
plc.open()
plc.start()
Error handling
EtherNet/IP raises these errors for common failures:
| Error | Meaning |
|---|
RuntimeError: EtherNet/IP support requires the native package 'instro-ethernetip-python' | The native PyO3 package is not installed. Install instro-unstable[ethernetip]. |
RuntimeError: EtherNet/IP client not connected. Call open() first. | A read or write occurred before open(). |
TypeError: Tag '{alias}' expected PLC kind ... | The PLC returned a different kind than the configured scalar data_type, including a structured value for a scalar tag. |
ValueError: Tag '{alias}' value ... is below write_min | A write was rejected by local bounds checking. |
ValueError: Tag '{alias}' raw value ... is out of range for ... | A raw integer write does not fit in the configured PLC integer type. |
Native EtherNetIpError | The underlying EtherNet/IP request failed because of a connection, CIP, or tag error. |
Native EtherNetIpBatchError | One tag in a read_tags() batch failed. Check subclasses such as TagNotFoundError, DataTypeMismatchError, and CipError. |
Known limitations
- EtherNet/IP support is unstable and installed through
instro-unstable[ethernetip].
- Hardware testing covers Allen-Bradley CompactLogix 5332E 1769-L32E.
- Route paths support local backplane hops only. Network hops are unsupported.
- Current testing covers one local backplane hop.
- Tag discovery is unsupported.
- Arrays and UDTs are unsupported.
- String tags cannot be streamed to Nominal Core or Nominal Connect; read or write them manually.