A modern, cross-platform EtherNet/IP/CIP explicit-messaging SDK for direct CompactLogix and ControlLogix tag access, built in Rust for native performance, memory safety, and asynchronous I/O, with first-class APIs for Rust, .NET/C#, Python, C, and C++.
Driver scope: The library originates request/response CIP explicit messages for Logix tag reads, writes, batches, discovery, and routing. It is not a cyclic Class 1 implicit-I/O Scanner or Adapter. Registering an EtherNet/IP TCP session on port 44818 also does not mean that the library has opened a connected CIP Class 3 connection; the active tag-access path uses primarily Unconnected Send messaging.
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.
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.
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.
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.
- Current stable release:
1.2.1(crates.io + NuGet + PyPI) - Previous stable release:
1.2.0(tagged 2026-07-10) - Earlier stable releases:
1.1.0,1.0.0,0.7.0 - Real-hardware validation evidence is included for the release
Release snapshot:
1.2.1is a backwards-compatible patch release. A clone-shared controller-schema generation backs a comprehensiverefresh_schema()operation; array reads validate cached packed-BOOL addressing against controller responses and bound schema-drift recovery to one evict-and-retry, with writes never replayed after an ambiguous outcome. Rust, C, C#, Python, and C++ share the same explicit schema-refresh workflow and diagnostics (C FFI ABI is now v3, additive,CAP_SCHEMA_REFRESH). Pythonwrite_tags()now native-batches safe atomic scalar/array writes through the real Multiple Service Packet endpoint, matching Rust/C#/C++ throughput. Fixedget_tag_attributes/get_udt_definitionfailing outright on controllers that reject the per-tag Get Attribute List request. SeeCHANGELOG.md.- Hardware-validated live on ControlLogix
1756-L75firmware33through a1756-EN2Tbridge: the schema-change gate (online array-shape swap and offline UDT layout-edit, both directions, all four bindings), a post-merge four-binding full-coverage rerun (2304/2304 reads, 2285/2285 writes, 0 anomalies), and a cross-binding performance rerun (zero failures; all four bindings converge within ~0.08 ms at batch size 100) all pass. Seedocs/validation/2026-08-22_1756-L75_fw33_schema-change-gate.md. 1.2.0was a minor (non-breaking) release: behavioral fixes, deprecations, and additive surface with no Rust-API signature breaks. Highlights: handle-aware STRING writes, CIP fragmentation for large strings/structures, packet-size-aware batch grouping, and first-class C/C++ consumer support. The C FFI ABI was v2 at that time. Full-coverage hardware exercisers passed on CompactLogix 5069-L330ERM fw38 across Rust/C#/Python/C++: 2304/2304 reads, 2285/2285 writes, 2285/2285 verify, 0 unexpected anomalies. Seedocs/validation/2026-07-08_cross-binding_full-coverage_5069-L330ERM_fw38.md.- The real-hardware compatibility matrix and contributor test program tracks exact processor/firmware/binding evidence and defines 24-hour endurance and performance characterization profiles.
- crates.io ships five workspace artifacts at
1.2.1:rust-ethernet-ip-types,rust-ethernet-ip-tag-path,rust-ethernet-ip-protocol,rust-ethernet-ip-udt, and the top-levelrust-ethernet-ip. NuGet shipsRustEtherNetIp 1.2.1and PyPI shipsrust-ethernet-ip 1.2.1from the GitHub release workflow on tag push.
Inspired by the useful "Tier One platform" convention used by mature native libraries, Tier 1 here means a target is a blocking automated release gate. It does not mean that every CompactLogix or ControlLogix model has been tested. Exact controller and firmware evidence is tracked separately in the real-hardware compatibility matrix.
| Target | Platforms/toolchains | What the blocking gate exercises | Tier 1 |
|---|---|---|---|
| Rust core | Ubuntu, Windows, macOS; stable and beta | Format, Clippy, complete workspace tests, all features | Yes |
| Rust MSRV | Ubuntu; Rust 1.88 | Complete workspace tests with all features | Yes |
| C# wrapper | Ubuntu, Windows, macOS; .NET 10 | Managed tests plus native P/Invoke integration tests | Yes |
| Python wrapper | Ubuntu, Windows, macOS; Python 3.10–3.12 | Import, source compilation, unit and simulator-backed integration tests | Yes |
| C/C++ ABI and example | Ubuntu, Windows, and macOS; C++17/CMake | Header/export parity, link test, RAII smoke example, full-coverage runner build | Yes |
| Package assembly | Linux x64, Windows x64, macOS arm64 | Cargo package, NuGet pack, Python wheel build/install/import | Yes |
| Real PLC release gate | 1756-L75 firmware 33 | Full read/write/read-back manifest plus schema-change gate in Rust, C#, Python, and C/C++ | Yes, for 1.2.1 |
“Yes” records the required target and scope, not the latest GitHub Actions run. Before relying on a commit, confirm its checks are green. New platforms become Tier 1 only after repeatable CI coverage exists; community-tested combinations remain in the hardware matrix until promoted into a release gate.
- 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
- 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
| Application need | Recommended pattern |
|---|---|
| One measurement, command, or occasional setpoint | Typed single-tag read/write |
| Several independent values in one scan | Batch read/write; inspect every per-tag result |
| One known UDT field | Read or write its full symbolic member path |
| One logical snapshot of an entire UDT | Whole-UDT read; fragmented reads handle large structures |
| Change a UDT | Prefer member-level writes; whole writes require the exact template-compatible representation |
| Controller tag | Use TagName |
| Program tag | Use Program:<program-name>.TagName with the same read/write API |
Batches reduce network round trips but are not atomic PLC transactions. Whole
UDT-array-element reads work; whole-element writes are not supported in 1.2.0,
so write paths such as Motors[0].CommandSpeed individually. The language
guides contain complete examples: C#,
Python, and C/C++.
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. STRINGmembers inside UDTs — built-inSTRINGand custom string types (own name/length, e.g.Str82/Str400) — write and read through the normal string APIs as of1.2.0: the library discovers the target's real structure handle instead of assuming the built-in0x0FCE. Strings larger than one CIP packet use CIP fragmentation. Seedocs/STRING_HANDLING.md.- The built-in Logix
STRINGstores text inSINT DATA[82], so its capacity is 82 bytes. Custom string types use their declaredDATA[N]capacity. The approximately 494-byte measured single-request ceiling includes CIP service and path overhead; it is not a universal text limit, and 1.2.0 fragments larger transfers.
Real-hardware note from the 0.7.0 release validation:
- Validated on
5069-L320ERMS3, firmware35, at192.168.0.1:44818 - Validated on
1756-L81ES, firmware37, via1756-EN3TRslot0at192.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
STRINGwrites succeed when encoded as the Logix structure type (0x02A0+0x0FCEhandle). - 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), UDTSTRINGmembers — built-in and custom string types — also write+read directly via handle-aware writes; the earlier0x2107rejections were a structure-handle mismatch, not a firmware block.
Detailed technical background and examples:
- AB String/UDT write limitations
- CompactLogix real-PLC validation record
- CompactLogix C# wrapper validation record
- ControlLogix real-PLC validation record
- ControlLogix C# wrapper validation record
[dependencies]
rust-ethernet-ip = "1.2.1"
tokio = { version = "1", features = ["full"] }<PackageReference Include="RustEtherNetIp" Version="1.2.1" />Or from the CLI:
dotnet add package RustEtherNetIp --version 1.2.1Current NuGet packaging note:
RustEtherNetIp1.2.1is published on NuGet- the package bundles native runtimes for
win-x64,linux-x64, andosx-arm64 - the managed package currently targets
.NET 10
pip install rust-ethernet-ip==1.2.1The 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:
- python/README.md
- Integration and deployment guide
- docs/PYTHON_WRAPPER_STRATEGY.md
- docs/DOCKER_EXAMPLE_STACKS.md
Build the native library and include the checked-in C header:
cargo build --release --features ffi --lockedUse 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. The C ABI is
the complete native wrapper boundary; the example RAII class is intentionally a
smaller convenience layer, not yet a full C++ SDK.
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
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(())
}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}");
}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(...)andwrite_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.
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`
}
}
}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;
}
};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 minimalThe C# Web HMI demo is the recommended first real-application walkthrough. It includes a React/TypeScript dashboard, ASP.NET Core backend, simulation mode, exact Studio 5000 tag definitions, and direct or routed live-PLC instructions.
cd examples/CSharpWebHmi && ./run-demo.sh
cd examples/WpfExample && dotnet run
cd examples/WinFormsExample && dotnet run
cd examples/AspNetExample && dotnet runcargo run --example comprehensive_terminal_demo
cargo run --example stream_injection_example
cargo run --example test_discover_and_verifyPYTHONPATH=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 --buildcargo 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- API docs (docs.rs)
- Programmer manual (Rust + C#)
- Integration and deployment guide
- Python wrapper guide
- C/C++ integration guide
- Wrapper and native-platform gap analysis
- Official sources traceability
- PLC/simulator compatibility matrix (0.7.0)
- C# wrapper guide
- Tag introspection
- Troubleshooting
- Changelog
- Instro uses
rust-ethernet-ipin its EtherNet/IP integration for Allen-Bradley PLCs.
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
See CONTRIBUTING.md.
MIT. See LICENSE.
This software is provided "AS IS". Validate thoroughly in your own environment before production deployment, especially for industrial control systems.