EtherNet/IP#
Using EtherNet/IP for Allen-Bradley PLC tag communication
Note
The Nominal Instrumentation Library is in alpha testing. To request access, open the Support Portal.
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.
Note
The EtherNet/IP client lives under instro.ethernetip and is provided by the instro-ethernetip package (a native backend). Install it with the instro[ethernetip] extra; it is also included in instro[all].
Install#
Install with the EtherNet/IP extra:
uv add "instro[ethernetip]"
or with pip:
pip install "instro[ethernetip]"
The standalone package also installs directly: pip install instro-ethernetip.
Note
EtherNet/IP graduated out of the experimental instro-unstable package. If you used a pre-release
build, update imports from instro.unstable.ethernetip to instro.ethernetip (and the native
module from instro.unstable._ethernetip to instro.ethernetip._ethernetip), and install
instro[ethernetip] instead of 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.ethernetip import EtherNetIPDevice
RID = "<dataset_rid>" # Nominal Core dataset RID.
# autostart opens the session and starts polling.
with EtherNetIPDevice(config="compactlogix.json", autostart=True) as plc:
# 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)
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
}
]
}
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 tags marked |
Streaming to Nominal Core or Nominal Connect |
Boolean and numeric scalar tag values |
Manual tag operations |
Single-tag and batched non-string scalar reads, plus writes, through the native session API |
Unsigned integer validation |
|
Tag discovery |
Not supported |
UDTs |
Not supported |
Arrays |
Not supported |
Note
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 |
|
Poll configured tags automatically |
|
Reject out-of-range setpoints before PLC writes |
|
Reach a PLC CPU behind a local chassis/backplane |
|
Read several PLC tags in one native request |
|
Key concepts#
Lifecycle#
The EtherNet/IP workflow has six steps:
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.
Prefer a with statement for normal scripts so close() runs when the block exits. close() stops the background polling daemon, closes publishers, and closes the native EtherNet/IP session.
Tip
Pass autostart=True to combine steps 3 and 4 by starting a background polling thread (Background polling). A timing section in the config must be provided when using autostart=True.
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 |
|---|---|---|---|
|
|
Yes |
Boolean reads are published as numeric samples. |
|
|
Yes |
8-bit signed integer. |
|
|
Yes |
16-bit signed integer. |
|
|
Yes |
32-bit signed integer. |
|
|
Yes |
64-bit signed integer. |
|
|
Yes |
8-bit unsigned integer. Implemented, but not validated. |
|
|
Yes |
16-bit unsigned integer. Implemented, but not validated. |
|
|
Yes |
32-bit unsigned integer. Implemented, but not validated. |
|
|
Yes |
64-bit unsigned integer. Implemented, but not validated. |
|
|
Yes |
32-bit floating point. |
|
|
Yes |
64-bit floating point. |
|
N/A |
No |
Allen-Bradley Logix |
Warning
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#
Allen-Bradley Logix STRING tags are encoded as LEN (DINT) plus DATA (SINT[]), which is unsupported at this time. Furthermore, string measurements are not supported by Nominal Core or Nominal Connect.
Batched reads#
Use the private native session to read several PLC tags in one request:
from instro.ethernetip._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:
boolsigned and unsigned integer types
reallreal
String tags, UDTs, and arrays are not part of the Python EtherNet/IP polling surface.
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.
Warning
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
aliasthe PLC
tag_namethe expected scalar
data_typeoptional
poll,write_min, andwrite_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": "motor_running",
"tag_name": "MotorRunning",
"data_type": "bool",
"description": "Motor run feedback",
"poll": true
},
{
"alias": "line_speed",
"tag_name": "LineSpeed",
"data_type": "real",
"description": "Measured line speed in rpm",
"poll": true
},
{
"alias": "part_count",
"tag_name": "PartCount",
"data_type": "dint",
"description": "Completed parts since reset",
"poll": true
},
{
"alias": "speed_setpoint",
"tag_name": "SpeedSetpoint",
"data_type": "real",
"description": "Commanded line speed in rpm",
"poll": false,
"write_min": 0.0,
"write_max": 2500.0
}
]
}
Device section#
The device section describes the physical PLC. EtherNetIPDevice uses the name field as a prefix for published channel names.
Field |
Required |
Description |
|---|---|---|
|
Yes |
Device name, used as channel name prefix |
|
No |
Human-readable description |
|
No |
Device manufacturer |
|
No |
Device model number |
Connection section#
Field |
Required |
Default |
Description |
|---|---|---|---|
|
Yes |
- |
PLC IP address or hostname |
|
No |
|
EtherNet/IP TCP port |
|
No |
- |
Optional local backplane route path |
Route path#
Field |
Required |
Default |
Description |
|---|---|---|---|
|
No |
|
Ordered list of local backplane hops |
Each hop must be:
Field |
Required |
Description |
|---|---|---|
|
Yes |
Must be |
|
Yes |
Backplane slot number, 0-255 |
Tag definitions#
Each tag entry defines one aliasable PLC tag:
Field |
Required |
Default |
Description |
|---|---|---|---|
|
Yes |
- |
Unique local alias used in |
|
Yes |
- |
PLC tag name |
|
Yes |
- |
Expected scalar PLC data type |
|
No |
- |
Optional human-readable description |
|
No |
|
Include this tag in background polling |
|
No |
- |
Minimum allowed write value for numeric tags |
|
No |
- |
Maximum allowed write value for numeric tags |
Timing section#
Field |
Required |
Default |
Description |
|---|---|---|---|
|
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:
protocolmust be"ethernetip".Tag aliases must be unique.
Every tag must declare
data_type.data_typemust be one of the supported scalar types.Only numeric tags can use
write_minandwrite_max.write_minmust be less than or equal towrite_max.Integer write limits must fit in the configured PLC integer type.
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#
from instro.ethernetip import EtherNetIPDevice
# Uses the connection section from compactlogix.json.
with EtherNetIPDevice(config="compactlogix.json") as plc:
# Assumes "line_speed" is a specified tag in compactlogix.json
# and that 1200.0 is a valid value.
measurement = plc.read_tag("line_speed")
plc.write_tag("line_speed", 1200.0)
from instro.ethernetip import EtherNetIPDevice
with 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}]},
},
) as plc:
measurement = plc.read_tag("line_speed")
from instro.lib.types import DeviceInfo
from instro.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.
with EtherNetIPDevice(config, connection=connection, autostart=True) as plc:
measurement = plc.read_tag("line_speed")
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.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 |
|---|---|
|
The native PyO3 package is not installed. Install |
|
A read or write occurred before |
|
The PLC returned a different kind than the configured scalar |
|
A write was rejected by local bounds checking. |
|
A raw integer write does not fit in the configured PLC integer type. |
Native |
The underlying EtherNet/IP request failed because of a connection, CIP, or tag error. |
Native |
One tag in a |
Known limitations#
EtherNet/IP support is installed through
instro[ethernetip](or the standaloneinstro-ethernetippackage).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.
PLC string tags are not exposed through the Python EtherNet/IP API.