Skip to main content
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()

Current support

EtherNet/IP support is intentionally narrow. It targets PLC tags whose names and scalar data types are known before runtime.
AreaSupported today
PLC familyTested on Allen-Bradley CompactLogix 5332E 1769-L32E
TransportEtherNet/IP explicit messaging over TCP
Route pathsDirect connection, or local backplane slot hops only
Background pollingAutomatic batched reads for all poll: true tags
Streaming to Nominal Core or Nominal ConnectBoolean and numeric scalar tag values
Manual tag operationsSingle-tag and batched scalar reads, plus writes, through the native session API
Unsigned integer validationusint, uint, udint, and ulint are implemented, but not validated
Tag discoveryNot supported
UDTsNot supported in the config-driven API
ArraysNot 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.
RequirementUse
Stream live scalar PLC data to Nominal Core or Nominal Connecttiming.poll_interval + tags with poll: true
Poll configured tags automaticallytiming.poll_interval + tags with poll: true
Reject out-of-range setpoints before PLC writeswrite_min / write_max on numeric tags
Reach a PLC CPU behind a local chassis/backplaneconnection.route_path.hops with type: "backplane"
Read or write a string tag manuallyEtherNetIpSession.read_tag() / EtherNetIpSession.write_tag()
Read several PLC tags in one native requestEtherNetIpSession.read_tags([...])

Key concepts

Lifecycle

The EtherNet/IP workflow has six moves:
  1. Define PLC tags in a JSON config file or an EtherNetIPConfig.
  2. Instantiate EtherNetIPDevice(config) with an optional connection override.
  3. Call open() to establish the native EtherNet/IP session.
  4. Call start() to begin background polling when timing is configured.
  5. Read or write tags by alias with type validation.
  6. 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 typePython payloadStreaming to Nominal Core or Nominal ConnectNotes
boolbool read as 0 or 1 in measurementsYesBoolean reads are published as numeric samples.
sintintYes8-bit signed integer.
intintYes16-bit signed integer.
dintintYes32-bit signed integer.
lintintYes64-bit signed integer.
usintintYes8-bit unsigned integer. Implemented, but not validated.
uintintYes16-bit unsigned integer. Implemented, but not validated.
udintintYes32-bit unsigned integer. Implemented, but not validated.
ulintintYes64-bit unsigned integer. Implemented, but not validated.
realfloatYes32-bit floating point.
lrealfloatYes64-bit floating point.
stringstrNoAllen-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.
FieldRequiredDescription
nameYesDevice name, used as channel name prefix
descriptionNoHuman-readable description
manufacturerNoDevice manufacturer
modelNoDevice model number

Connection section

FieldRequiredDefaultDescription
hostYes-PLC IP address or hostname
portNo44818EtherNet/IP TCP port
route_pathNo-Optional local backplane route path

Route path

FieldRequiredDefaultDescription
hopsNo[]Ordered list of local backplane hops
Each hop must be:
FieldRequiredDescription
typeYesMust be "backplane"
slotYesBackplane slot number, 0-255

Tag definitions

Each tag entry defines one aliasable PLC tag:
FieldRequiredDefaultDescription
aliasYes-Unique local alias used in read_tag() and write_tag()
tag_nameYes-PLC tag name
data_typeYes-Expected scalar PLC data type
descriptionNo-Optional human-readable description
pollNotrueInclude this tag in background polling
write_minNo-Minimum allowed write value for numeric tags
write_maxNo-Maximum allowed write value for numeric tags

Timing section

FieldRequiredDefaultDescription
poll_intervalYes-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

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()

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:
ErrorMeaning
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_minA 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 EtherNetIpErrorThe underlying EtherNet/IP request failed because of a connection, CIP, or tag error.
Native EtherNetIpBatchErrorOne 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.