Skip to main content

Rust EtherNet/IP for Python

rust-ethernet-ip is a thin Python interface to the Rust EtherNet/IP core for Allen-Bradley CompactLogix and ControlLogix controllers. It fits data acquisition, analytics, scientific workflows, ML feature collection, historian feeds, and lightweight API services that need direct Logix tag access.

Release Status

  • Latest published PyPI package: 1.2.0
  • Repository development line: 1.2.1 (not published yet)
  • Supported Python versions: 3.10–3.12
  • Native ABI used by 1.2.0: version 2

The wheel contains the native Rust library. The 1.2.0 real-hardware gate validated the Python wrapper alongside Rust, C#, and C/C++ on a CompactLogix 5069-L330ERM firmware 38, including controller/program paths, batches, scalar types, built-in/custom STRING members, arrays, and nested UDT-member paths.

Install

python -m pip install rust-ethernet-ip==1.2.0

Optional example dependencies:

python -m pip install 'rust-ethernet-ip[analytics]==1.2.0'  # pandas
python -m pip install 'rust-ethernet-ip[api]==1.2.0'        # FastAPI
python -m pip install 'rust-ethernet-ip[mqtt]==1.2.0'       # MQTT

Start Here

Use a context manager so the EtherNet/IP session is always unregistered:

from rust_ethernet_ip import Client

with Client("192.168.0.10:44818") as plc:
    count = plc.read_tag("ProductionCount")
    temperature = plc.read_tag("TankTemperature")
    running = plc.read_tag("MachineRunning")
    recipe = plc.read_string("RecipeName")

    plc.write_tag("ProductionSetpoint", 1250)
    plc.write_tag("TemperatureSetpoint", 72.5)
    plc.write_tag("EnableCommand", True)
    plc.write_tag("RecipeName", "PRODUCT_A")

Python values infer these common Logix types:

  • boolBOOL
  • 32-bit-range intDINT
  • larger signed intLINT
  • floatREAL
  • strSTRING

Use value_type when inference is ambiguous or a narrower/unsigned type is required:

plc.write_tag("SmallCounter", 123, value_type="INT")
plc.write_tag("UnsignedCount", 4_000_000_000, value_type="UDINT")
plc.write_tag("PrecisionValue", 1.23456789, value_type="LREAL")

Choose Single, Batch, or Structure Access

Need Best starting API Why
One measurement, command, or occasional setpoint read_tag / write_tag Smallest and clearest unit of work
Several independent values for one analytics sample read_tags Uses native packet-size-aware batch reads and returns values by tag name
Several writes with accurate per-tag status write_tags Native-batches safe atomic writes and retains typed sequential fallbacks for special cases
One known UDT member read_tag / write_tag with the full member path Avoids transferring or reconstructing the entire UDT
Inspect a whole UDT read_tag("Mixer") Returns decoded members when available, otherwise its raw symbol_id and bytes
Change an entire UDT Usually do not; write known members individually Whole writes require the exact template-compatible raw representation

Batching is useful for collector, dataframe, historian, and model-feature samples that read many independent tags together. It is not an atomic PLC transaction. For one value, use the single-tag API; for normal UDT commands, use full member paths.

STRING Support in 1.2.0

STRING writes are handle-aware. The same write_tag call supports top-level built-in STRING tags and built-in/custom STRING members addressed by full path:

with Client("192.168.0.10:44818") as plc:
    plc.write_tag("RecipeName", "PRODUCT_A")
    plc.write_tag("Mixer.Description", "Primary mixer")
    plc.write_tag("Motors[0].Description", "Infeed conveyor")

    print(plc.read_string("Motors[0].Description"))

The Studio 5000 built-in STRING is a structure with a 4-byte LEN and SINT DATA[82], plus alignment, so its text capacity is 82 bytes. Python strings are encoded as UTF-8; a non-ASCII character may use multiple bytes. Custom Logix string types use their declared DATA[N] capacity and a distinct structure handle, which the library discovers automatically.

Real hardware confirms built-in STRING, custom Str82, and custom Str400 members on 5069-L330ERM firmware 38. The measured single unconnected request ceiling on that target is about 494 total bytes, including the tag path and CIP overhead—not 494 text bytes. Version 1.2.0 switches to fragmented CIP services when needed. A 600-byte custom string is simulator-confirmed; qualify very large custom strings on the exact controller and firmware before production.

Controller and Program-Scoped Tags

Known program tags use the Logix symbolic prefix directly:

with Client("192.168.0.10:44818") as plc:
    controller_value = plc.read_tag("ProductionCount")
    program_value = plc.read_tag("Program:MainProgram.ProductionCount")
    plc.write_tag("Program:MainProgram.ProductionSetpoint", 1250)

The Python 1.2.0 wrapper does not expose tag discovery or metadata APIs. It can read and write known controller/program paths, arrays, bits, and UDT members. Do not assume that program enumeration is available merely because direct program paths work.

Controller scope means the tag belongs to the controller and its path is just TagName. Program scope means it belongs to one Logix program and its path is Program:<program-name>.TagName. The value API is otherwise the same.

UDT Reads and Member Writes

with Client("192.168.0.10:44818") as plc:
    # A whole structure snapshot. Depending on metadata, this is decoded
    # members or a dictionary containing symbol_id and raw data bytes.
    mixer = plc.read_tag("Mixer")
    print(mixer)

    # Prefer complete member paths for normal application logic.
    speed = plc.read_tag("Mixer.SpeedFeedback")
    plc.write_tag("Mixer.SpeedSetpoint", 60.0)
    plc.write_tag("Mixer.Enabled", True)
    plc.write_tag("Mixer.Description", "Primary mixer")

    # Whole array-element reads work; write its members individually.
    motor = plc.read_tag("Motors[0]")
    plc.write_tag("Motors[0].CommandSpeed", 1250)

Do not construct an arbitrary Python dictionary and treat it as a whole UDT write. A safe whole-structure write needs the exact controller template handle and binary layout. Whole UDT-array-element writes are not supported in 1.2.0.

Batch Reads and Writes

from rust_ethernet_ip import BatchReadError, BatchWriteItem, Client

with Client("192.168.0.10:44818") as plc:
    try:
        values = plc.read_tags([
            "ProductionCount",
            "TankTemperature",
            "Program:MainProgram.MachineRunning",
        ])
    except BatchReadError as exc:
        print("Successful values:", exc.partial_values)
        print("Per-tag errors:", exc.errors)
        raise

    results = plc.write_tags([
        BatchWriteItem("ProductionSetpoint", 1250),
        BatchWriteItem("TemperatureSetpoint", 72.5),
        BatchWriteItem("EnableCommand", True),
        BatchWriteItem("RecipeName", "PRODUCT_A"),
        BatchWriteItem("SmallCounter", 123, value_type="INT"),
    ])

    for tag, result in results.items():
        print(tag, "ok" if result.success else result.error)

read_tags uses the native batch-read path. write_tags combines contiguous atomic scalar and numeric array-element writes into native Multiple Service Packets. STRING/custom STRING, whole UDT, member/bit, packed BOOL array-element, and duplicate-name writes retain the typed sequential path. Mixed inputs execute in input order, although one native-safe run may contain several operations. The return value remains keyed by tag name; when a name is repeated, every write executes sequentially and the final result for that name is retained.

ControlLogix Routing

Connect to the Ethernet module and supply the CPU backplane slot:

from rust_ethernet_ip import Client, RoutePath

route = RoutePath(slots=[0])
with Client("192.168.0.20:44818", route_path=route) as plc:
    print(plc.read_tag("ProductionCount"))

For an ordered multi-hop route, use explicit hops:

from rust_ethernet_ip import Client, RouteHop, RoutePath

route = RoutePath(hops=[
    RouteHop.backplane(slot=3),
    RouteHop.ethernet("192.168.10.20", port=2),
    RouteHop.backplane(slot=0),
])

with Client("192.168.0.20:44818", route_path=route) as plc:
    print(plc.read_tag("ProductionCount"))

CompactLogix controllers with built-in Ethernet normally do not need a route.

Health and Diagnostics

with Client("192.168.0.10:44818") as plc:
    plc.read_tag("ProductionCount")
    print("healthy:", plc.check_health())

    snapshot = plc.get_diagnostics_snapshot(detailed=True)
    print("reads:", snapshot.operations.total_reads)
    print("failed reads:", snapshot.operations.failed_reads)
    print("average latency:", snapshot.performance.avg_read_latency_ms)
    print("last error:", snapshot.errors.last_error_message)
    print("schema generation:", snapshot.schema_cache.generation)

CPU and memory values are placeholders in this release. Connection, operation, error, latency, and verified-health metrics are the useful fields.

For an online tag replacement or controller download, pause application writes, complete the PLC change, call plc.refresh_schema(), optionally rediscover and verify critical reads, and then resume writes. This clears all schema-derived native caches without reconnecting.

Error Handling

from rust_ethernet_ip import (
    BatchReadError,
    NativeLibraryLoadError,
    PlcConnectionError,
    PlcOperationError,
)

try:
    with Client("192.168.0.10:44818") as plc:
        plc.write_tag("ReadOnlyTag", 42)
except NativeLibraryLoadError as exc:
    print("Native package problem:", exc)
except PlcConnectionError as exc:
    print("Connection problem:", exc)
except BatchReadError as exc:
    print(exc.partial_values, exc.errors)
except PlcOperationError as exc:
    print("PLC/CIP operation problem:", exc)

PlcOperationError includes the native last-error reason when available.

Example Catalog

Core wrapper examples:

Data and application examples:

Set RUST_ETHERNET_IP_PLC_ADDRESS for the examples. Routed examples also use RUST_ETHERNET_IP_PLC_SLOT.

export RUST_ETHERNET_IP_PLC_ADDRESS=192.168.0.10:44818
PYTHONPATH=python python3 python/examples/read_single_tag.py

Collector, API, and MQTT Examples

PYTHONPATH=python python3 python/examples/collector_service.py \
  --config python/examples/collector_config.example.json --once

PYTHONPATH=python python3 python/examples/mqtt_publisher_example.py \
  --config python/examples/mqtt_publisher_config.example.json --once

The collector writes timestamped batch snapshots to CSV or SQLite. The MQTT example publishes normalized snapshots to factory/{site}/plc/{plc_name}/snapshot. A Docker example stack is available:

docker compose -f docker/python-stack/docker-compose.yml up --build

Local Repository Development

From the repository root:

cargo build --features ffi --example python_test_simulator
PYTHONPATH=python python3 -m unittest discover -s python/tests
RUST_ETHERNET_IP_START_SIM=1 PYTHONPATH=python \
  python3 -m unittest discover -s python/tests

If the native library is outside the usual target/debug or target/release location, set RUST_ETHERNET_IP_NATIVE_LIB to its absolute path.

Current Boundaries

  • Python is intentionally synchronous and thin; Rust owns protocol behavior.
  • Program-scoped enumeration, subscriptions, and tag-group APIs are not exposed in Python 1.2.0.
  • Whole UDT-array-element writes are not supported; write known members by their full paths.
  • This package targets CompactLogix and ControlLogix EtherNet/IP tag access, not Modbus TCP or a general OPC server.

See the integration guide, hardware matrix, and 1.2.0 validation record.

Use GitHub Issues for reproducible defects and GitHub Discussions for integration questions. The package is MIT licensed.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rust_ethernet_ip-1.2.1.tar.gz (28.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

rust_ethernet_ip-1.2.1-py3-none-win_amd64.whl (794.0 kB view details)

Uploaded Python 3Windows x86-64

rust_ethernet_ip-1.2.1-py3-none-manylinux_2_28_x86_64.whl (985.2 kB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

rust_ethernet_ip-1.2.1-py3-none-macosx_11_0_universal2.whl (830.0 kB view details)

Uploaded Python 3macOS 11.0+ universal2 (ARM64, x86-64)

File details

Details for the file rust_ethernet_ip-1.2.1.tar.gz.

File metadata

  • Download URL: rust_ethernet_ip-1.2.1.tar.gz
  • Upload date:
  • Size: 28.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.12.14

File hashes

Hashes for rust_ethernet_ip-1.2.1.tar.gz
Algorithm Hash digest
SHA256 819acbcf5aeba6d26fe43918de302dd46710ecb35806cc5903247da77f30674d
MD5 a9532abe1284a48a5fbda372e368f9ec
BLAKE2b-256 b1f7389f792e81b201a5f7c1519e95819c6d40ba8931f41af7d4d61fb2e7b7bc

See more details on using hashes here.

File details

Details for the file rust_ethernet_ip-1.2.1-py3-none-win_amd64.whl.

File metadata

File hashes

Hashes for rust_ethernet_ip-1.2.1-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 ec46571472cec9cc95d4988412b9febc8b8ba2c6b0cb472dcabea591416de9c6
MD5 80d0336536bdaf99b8dd620d5adfc149
BLAKE2b-256 00421eec223df7dc535a3b59fd2182f4e6195817a5d115d0c328db6a204b5ec7

See more details on using hashes here.

File details

Details for the file rust_ethernet_ip-1.2.1-py3-none-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for rust_ethernet_ip-1.2.1-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 16638de4ae17aedb22fcba47e8374185946f006d77fba3b513f5b701d96c2f98
MD5 f6b13e97a7bf190aa8d0d1e0480c3c47
BLAKE2b-256 19a0fe63a4978e99a06552ff065176f8ec245c8b9d7cb32939d663ff5c73fd7e

See more details on using hashes here.

File details

Details for the file rust_ethernet_ip-1.2.1-py3-none-macosx_11_0_universal2.whl.

File metadata

File hashes

Hashes for rust_ethernet_ip-1.2.1-py3-none-macosx_11_0_universal2.whl
Algorithm Hash digest
SHA256 921a1649bc02b268b1e4af57edf362ab4335cfed73707e32def565704e41843c
MD5 5d99ba28f3d2bac3db29f255554039a0
BLAKE2b-256 d6e4be75ffa9e395217eafadc7e2c88f05c8ac4d0f8e021f586547c8772ab636

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.2.1 This release

4 files

1.2.0

4 files

1.1.0

4 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page