rust-ethernet-ip 1.2.0

High-performance EtherNet/IP communication library for Allen-Bradley CompactLogix and ControlLogix PLCs
Documentation

Crates.io Rust License Documentation

Production-focused EtherNet/IP library for Allen-Bradley CompactLogix and ControlLogix PLCs.

Why this project exists

Why Rust for the core

EtherNet/IP runs on factory floors where a dropped packet or an out-of-bounds parse can stop a production line. Rust was chosen for the core because it provides:

  • memory safety with no garbage collector — no GC pauses during high-rate scan loops
  • predictable latency and low overhead, important for sub-100 ms tag polling
  • a strong type system that pushes wire-protocol mistakes to compile time instead of to runtime in front of a real PLC
  • a single statically-linked binary that drops into industrial PCs and edge gateways without a managed runtime

The same library can therefore serve both the embedded edge — where C and C++ have historically dominated — and higher-level integrations, without rewriting the protocol layer for each consumer.

Why a C# wrapper

The Allen-Bradley world is overwhelmingly a Windows and .NET world: HMIs, MES integrations, SCADA front-ends, OPC servers, and integrator-built operator software are usually written in C#. Most engineers on the plant floor are not going to write Rust, and they should not have to. The NuGet-packaged RustEtherNetIp wrapper lets those teams consume the Rust core through a familiar API (client.ReadDint("Tag")) while the protocol work still runs in the native layer.

Why a Python wrapper

Data engineering, analytics, historian ingestion, MES bridges, and machine learning on the plant floor are predominantly Python. A Python wrapper means a data scientist or integration engineer can pull live PLC data into pandas, into a Kafka producer, or into a Docker-deployed collector service, without rewriting the protocol stack or routing through OPC.

Vision and open source

There is no widely-adopted, modern, open-source EtherNet/IP library for Allen-Bradley PLCs that is production-credible across the Rust, .NET, and Python ecosystems at the same time. Existing options tend to be closed-source vendor SDKs with restrictive licensing, aging C libraries with thin or stale language bindings, or per-team rewrites that never get hardened against real PLC firmware quirks.

This project exists to fill that gap with a single, MIT-licensed protocol implementation the industrial automation community can build on, audit, and extend — and to make the protocol details and controller-specific behavior (STRING structure encoding, UDT member writes, route-path quirks) explicit and documented rather than rediscovered by every new integrator.

Version Status

  • Current stable release: 1.2.0 (crates.io + NuGet + PyPI)
  • Previous stable release: 1.1.0 (tagged 2026-06-19)
  • Earlier stable releases: 1.0.0, 0.7.0
  • Real-hardware validation evidence is included for the release

Release snapshot:

  • 1.2.0 is a minor (non-breaking) release: behavioral fixes, deprecations, and additive surface with no Rust-API signature breaks. Highlights: handle-aware STRING writes so custom Logix string types (own name/length, e.g. Str82/Str400) read and write through the normal string APIs; CIP fragmentation (Read/Write Tag Fragmented) for strings/structures larger than one packet; packet-size-aware batch grouping (fixes large batch reads); first-class C/C++ consumer support (include/rust_ethernet_ip.h + CMake example); transport/session hardening, tag-addressing correctness, and diagnostics honesty. The C FFI ABI is now v2 (removes three unusable *mut EipClient exports; eip_abi_version() bumped) — the Rust API and the C#/Python packages are unaffected. See CHANGELOG.md.
  • Full-coverage hardware exercisers pass on CompactLogix 5069-L330ERM fw38 across Rust/C#/Python/C++: 2304/2304 reads, 2285/2285 writes, 2285/2285 verify, 0 unexpected anomalies (STRING members now written+verified via the handle-aware path). See docs/validation/2026-07-08_cross-binding_full-coverage_5069-L330ERM_fw38.md.
  • crates.io ships five workspace artifacts at 1.2.0: rust-ethernet-ip-types, rust-ethernet-ip-tag-path, rust-ethernet-ip-protocol, rust-ethernet-ip-udt, and the top-level rust-ethernet-ip. NuGet ships RustEtherNetIp 1.2.0 and PyPI ships rust-ethernet-ip 1.2.0 from the GitHub release workflow on tag push.

Project Focus

  • Rust core library
  • C# wrapper via NuGet (RustEtherNetIp)
  • Python wrapper for data collection, analytics, and service integrations
  • Industrial PC applications, with current NuGet packaging focused on Windows win-x64
  • Deterministic behavior and regression safety

Key Capabilities

  • Native support for all 13 common AB data types: BOOL, SINT, INT, DINT, LINT, USINT, UINT, UDINT, ULINT, REAL, LREAL, STRING, UDT
  • Advanced tag addressing: program-scoped tags, array indexing, bit access, nested UDT paths
  • Route path support for backplane/slot routing (ControlLogix)
  • Batch operations (read_tags_batch, write_tags_batch, execute_batch)
  • Tag-group polling API (upsert_tag_group, read_tag_group_once, subscribe_tag_group)
  • UDT discovery and metadata access
  • Real-time subscriptions and health-check APIs
  • Schema export and diagnostics snapshot surfaces
  • C# wrapper for .NET integration
  • Python wrapper and service/data-pipeline examples

Known PLC/Firmware Limitations

Some write behaviors depend on exact Logix wire encoding and controller firmware:

  • Direct writes to scalar UDT array element members (for example MyUdtArray[0].Speed) are confirmed writeable on 5069-L330ERM fw38 when the full member path is preserved.
  • STRING members inside UDTs — built-in STRING and custom string types (own name/length, e.g. Str82/Str400) — write and read through the normal string APIs as of 1.2.0: the library discovers the target's real structure handle instead of assuming the built-in 0x0FCE. Strings larger than one CIP packet use CIP fragmentation. See docs/STRING_HANDLING.md.

Real-hardware note from the 0.7.0 release validation:

  • Validated on 5069-L320ERMS3, firmware 35, at 192.168.0.1:44818
  • Validated on 1756-L81ES, firmware 37, via 1756-EN3TR slot 0 at 192.168.0.101:44818
  • On that CompactLogix target, normal reads/writes, route-path access, subscriptions, UDT reads, and batch operations are working
  • On that ControlLogix target, the same main read/write, route-path, subscription, UDT-read, and batch paths are working
  • On newer 2026-07-02 validation against 5069-L330ERM firmware 38, standalone standard STRING writes succeed when encoded as the Logix structure type (0x02A0 + 0x0FCE handle).
  • On 2026-07-03 validation against the same controller, all 60 scalar UDT-array-element-member writes succeeded. As of 1.2.0 (2026-07-08), UDT STRING members — built-in and custom string types — also write+read directly via handle-aware writes; the earlier 0x2107 rejections were a structure-handle mismatch, not a firmware block.

Detailed technical background and examples:

Installation

Rust

[dependencies]
rust-ethernet-ip = "1.2.0"
tokio = { version = "1", features = ["full"] }

C#

<PackageReference Include="RustEtherNetIp" Version="1.2.0" />

Or from the CLI:

dotnet add package RustEtherNetIp --version 1.2.0

Current NuGet packaging note:

  • RustEtherNetIp 1.2.0 is published on NuGet
  • the package bundles native runtimes for win-x64, linux-x64, and osx-arm64
  • the managed package currently targets .NET 10

Python

pip install rust-ethernet-ip==1.2.0

The wheel bundles the native library, so a plain pip install works with no separate build. (The Rust and C# wrappers ship alongside it from the same release.)

See:

C and C++

Build the native library and include the checked-in C header:

cargo build --release --features ffi --locked

Use include/rust_ethernet_ip.h for the stable C ABI, or the small RAII wrapper in examples/cpp/ for C++ projects. Qt applications should keep the blocking FFI calls on a worker QThread; see docs/CPP_INTEGRATION.md.

Integration and Deployment

If you are evaluating the library for production use, start here:

That guide covers:

  • when to use Rust vs C# vs Python
  • step-by-step integration into each stack
  • native runtime deployment expectations
  • routed ControlLogix usage
  • troubleshooting and rollout checks

Quick Start (Rust)

use rust_ethernet_ip::{EipClient, PlcValue, RoutePath};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Direct connect
    let mut client = EipClient::connect("192.168.1.100:44818").await?;

    // Or routed connect (example: ControlLogix slot 3)
    let route = RoutePath::new().add_slot(3);
    let mut routed = EipClient::with_route_path("192.168.1.100:44818", route).await?;

    let running = client.read_tag("Program:Main.MotorRunning").await?;
    client
        .write_tag("Program:Main.SetPoint", PlcValue::Dint(1500))
        .await?;

    let tags = vec!["Program:Main.Temp", "Program:Main.Pressure"];
    let batch = routed.read_tags_batch(&tags).await?;

    println!("running={running:?}, batch={batch:?}");
    Ok(())
}

Quick Start (C#)

using RustEtherNetIp;

using var client = new EtherNetIpClient();
if (client.Connect("192.168.1.100:44818"))
{
    bool running = client.ReadBool("Program:Main.MotorRunning");
    int count = client.ReadDint("Program:Main.ProductionCount");

    client.WriteBool("Program:Main.Start", true);
    client.WriteDint("Program:Main.SetPoint", 1500);

    Console.WriteLine($"running={running}, count={count}");
}

Batch Operations

use rust_ethernet_ip::{BatchOperation, PlcValue};

// Batch write
let writes = vec![
    ("SetPoint1", PlcValue::Real(72.5)),
    ("SetPoint2", PlcValue::Real(74.0)),
    ("Enable", PlcValue::Bool(true)),
];
let write_results = client.write_tags_batch(&writes).await?;

// Mixed batch
let ops = vec![
    BatchOperation::Read { tag_name: "ActualTemp".into() },
    BatchOperation::Write { tag_name: "SetPoint1".into(), value: PlcValue::Real(73.0) },
];
let mixed_results = client.execute_batch(&ops).await?;

Notes:

  • read_tags_batch(...) and write_tags_batch(...) preserve tag association in their return values.
  • execute_batch(...) may regroup mixed operations for packet optimization, so correlate results by the returned operation metadata rather than assuming strict mixed-input ordering.

Tag Group Event Handling

Rust

use rust_ethernet_ip::{EipClient, TagGroupEventKind};

let mut client = EipClient::connect("192.168.1.100:44818").await?;
client
    .upsert_tag_group(
        "cell_1",
        vec!["Program:Main.Temp".into(), "Program:Main.Pressure".into()],
        250,
    )
    .await?;

let sub = client.subscribe_tag_group("cell_1").await?;
while let Some(event) = sub.wait_for_update().await {
    match event.kind {
        TagGroupEventKind::Data => {
            // All tags read successfully
        }
        TagGroupEventKind::PartialError => {
            // Some tags failed; inspect per-tag `snapshot.values[*].error`
        }
        TagGroupEventKind::ReadFailure => {
            // Full cycle failed; inspect `event.error` and `event.failure`
        }
    }
}

C#

client.UpsertTagGroup("cell_1", new[] { "DINT_TAG", "PressureTag" }, updateRateMs: 250);
var group = client.SubscribeToTagGroup("cell_1");

group.PollingEvent += (_, evt) =>
{
    switch (evt.Kind)
    {
        case TagGroupEventKind.Data:
            // All tags good
            break;
        case TagGroupEventKind.PartialError:
            // Mixed quality; inspect evt.Errors per tag
            break;
        case TagGroupEventKind.ReadFailure:
            // Entire cycle failed; inspect evt.ErrorMessage + evt.Failure
            break;
    }
};

Build and Test

cargo fmt
cargo clippy -p rust-ethernet-ip --lib -- -D warnings
cargo test --workspace --all-targets
dotnet test csharp/RustEtherNetIp.Tests/RustEtherNetIp.Tests.csproj -v minimal

Examples

.NET

cd examples/WpfExample && dotnet run
cd examples/WinFormsExample && dotnet run
cd examples/AspNetExample && dotnet run

Rust

cargo run --example comprehensive_terminal_demo
cargo run --example stream_injection_example
cargo run --example test_discover_and_verify

Python

PYTHONPATH=python python3 python/examples/read_single_tag.py
PYTHONPATH=python python3 python/examples/collector_service.py --config python/examples/collector_config.example.json --once
docker compose -f docker/python-stack/docker-compose.yml up --build

C++

cargo build --release --features ffi --locked
cmake -S examples/cpp -B target/cpp -DRUST_ETHERNET_IP_NATIVE_LIB="$PWD/target/release/librust_ethernet_ip.so"
cmake --build target/cpp
ctest --test-dir target/cpp --output-on-failure

Documentation

Community and Support

Project collaboration is open for:

  • priority issue handling
  • priority feature sponsorship
  • integration support for real deployments
  • OEM and system-integrator feedback
  • companies willing to provide specific hardware access for validation

If your team wants to collaborate on one of those paths, start with a GitHub Discussion or issue and describe:

  • controller model and firmware
  • direct vs routed topology
  • target application type
  • required feature set and timeline

Contributing

See CONTRIBUTING.md.

License

MIT. See LICENSE.

Safety Notice

This software is provided "AS IS". Validate thoroughly in your own environment before production deployment, especially for industrial control systems.