rust-ethernet-ip 1.2.1

High-performance EtherNet/IP communication library for Allen-Bradley CompactLogix and ControlLogix PLCs
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/sergiogallegos/rust-ethernet-ip/main/images/brand/logo-dark.png">
    <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/sergiogallegos/rust-ethernet-ip/main/images/brand/logo-light.png">
    <img src="https://raw.githubusercontent.com/sergiogallegos/rust-ethernet-ip/main/images/brand/logo-light.png" alt="rust-ethernet-ip logo" width="420" />
  </picture>
</p>

[![Crates.io](https://img.shields.io/crates/v/rust-ethernet-ip.svg)](https://crates.io/crates/rust-ethernet-ip)
[![Rust](https://img.shields.io/badge/rust-1.88+-orange.svg)](https://www.rust-lang.org)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Documentation](https://docs.rs/rust-ethernet-ip/badge.svg)](https://docs.rs/rust-ethernet-ip)
[![Website](https://img.shields.io/badge/website-rustethernetip.com-cf6636.svg)](https://rustethernetip.com/)

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.

## 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)
- Next patch in preparation: `1.2.1` (post-1.2.0 fixes, API documentation, hardware test program, and project website; not yet published)
- 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`](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`](docs/validation/2026-07-08_cross-binding_full-coverage_5069-L330ERM_fw38.md).
- The [real-hardware compatibility matrix and contributor test program](docs/HARDWARE_COMPATIBILITY.md) tracks exact processor/firmware/binding evidence and defines 24-hour endurance and performance characterization profiles.
- 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.

## Release Validation Tiers

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](docs/HARDWARE_COMPATIBILITY.md).

| 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 | 5069-L330ERM firmware 38 | Full read/write/read-back manifest in Rust, C#, Python, and C/C++ | Yes, for `1.2.0` |

“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.

## 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

### Which access pattern should I use?

| 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#](csharp/RustEtherNetIp/README.md),
[Python](python/README.md), and [C/C++](docs/CPP_INTEGRATION.md).

## 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`](docs/STRING_HANDLING.md).
- The built-in Logix `STRING` stores text in `SINT DATA[82]`, so its capacity is
  82 bytes. Custom string types use their declared `DATA[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`, 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:
- [AB String/UDT write limitations](docs/AB_String_UDT_Write_Limitations.md)
- [CompactLogix real-PLC validation record](docs/validation/2026-04-07_real_plc_5069-L320ERMS3_fw35.md)
- [CompactLogix C# wrapper validation record](docs/validation/2026-04-07_csharp_wrapper_real_plc_5069-L320ERMS3_fw35.md)
- [ControlLogix real-PLC validation record](docs/validation/2026-04-16_real_plc_1756-L81ES_via_1756-EN3TR_slot0.md)
- [ControlLogix C# wrapper validation record](docs/validation/2026-04-16_csharp_wrapper_real_plc_1756-L81ES_via_1756-EN3TR_slot0.md)

## Installation

### Rust

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

### C#

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

Or from the CLI:

```bash
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

```bash
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:

- [python/README.md](python/README.md)
- [Integration and deployment guide](docs/INTEGRATION_AND_DEPLOYMENT.md)
- [docs/PYTHON_WRAPPER_STRATEGY.md](docs/PYTHON_WRAPPER_STRATEGY.md)
- [docs/DOCKER_EXAMPLE_STACKS.md](docs/DOCKER_EXAMPLE_STACKS.md)

### C and C++

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

```bash
cargo build --release --features ffi --locked
```

Use [`include/rust_ethernet_ip.h`](include/rust_ethernet_ip.h) for the stable C
ABI, or the small RAII wrapper in [`examples/cpp/`](examples/cpp/) for C++
projects. Qt applications should keep the blocking FFI calls on a worker
`QThread`; see [`docs/CPP_INTEGRATION.md`](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.

## Integration and Deployment

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

- [Integration and deployment guide](docs/INTEGRATION_AND_DEPLOYMENT.md)

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)

```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#)

```csharp
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

```rust
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

```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#

```csharp
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

```bash
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

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

### Rust

```bash
cargo run --example comprehensive_terminal_demo
cargo run --example stream_injection_example
cargo run --example test_discover_and_verify
```

### Python

```bash
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++

```bash
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

- [API docs (docs.rs)](https://docs.rs/rust-ethernet-ip)
- [Programmer manual (Rust + C#)](docs/programmer_manual.md)
- [Integration and deployment guide](docs/INTEGRATION_AND_DEPLOYMENT.md)
- [Python wrapper guide](python/README.md)
- [C/C++ integration guide](docs/CPP_INTEGRATION.md)
- [Wrapper and native-platform gap analysis](docs/audit/1.2.1_wrapper_and_platform_gap_analysis.md)
- [Official sources traceability](docs/OFFICIAL_SOURCES.md)
- [PLC/simulator compatibility matrix (0.7.0)](docs/compat/0.7.0_plc_simulator_compatibility_matrix.md)
- [C# wrapper guide](csharp/RustEtherNetIp/README.md)
- [Tag introspection](docs/tag_introspection.md)
- [Troubleshooting](docs/TROUBLESHOOTING.md)
- [Changelog](CHANGELOG.md)

## Community and Support

- [GitHub Issues](https://github.com/sergiogallegos/rust-ethernet-ip/issues)
- [GitHub Discussions](https://github.com/sergiogallegos/rust-ethernet-ip/discussions)
- [Discord](https://discord.gg/uzaM3tua)
- [Sponsor development](https://github.com/sponsors/sergiogallegos)

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](CONTRIBUTING.md).

## License

MIT. See [LICENSE](LICENSE).

## Safety Notice

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