| Crates.io | rust-ethernet-ip |
| lib.rs | rust-ethernet-ip |
| version | 0.6.2 |
| created_at | 2025-08-19 17:01:11.861083+00 |
| updated_at | 2026-01-25 03:12:49.065194+00 |
| description | High-performance EtherNet/IP communication library for Allen-Bradley CompactLogix and ControlLogix PLCs |
| homepage | https://github.com/sergiogallegos/rust-ethernet-ip |
| repository | https://github.com/sergiogallegos/rust-ethernet-ip |
| max_upload_size | |
| id | 1802183 |
| size | 11,422,222 |
A high-performance, production-ready EtherNet/IP communication library specifically designed for Allen-Bradley CompactLogix and ControlLogix PLCs. Built in pure Rust with focus on PC applications, offering exceptional performance, memory safety, and comprehensive industrial features.
๐ฆ Available on crates.io
We are focused on the .NET stack (C# wrappers and examples) for production-quality industrial automation applications.
RustEtherNetIp.dll)This focused approach ensures we deliver a robust, well-tested, production-ready .NET integration for industrial automation systems.
This library is specifically designed for:
connect_with_stream() for custom TCP transport
TEST_PLC_ADDRESS - Set PLC IP address for testsTEST_PLC_SLOT - Set CPU slot numberSKIP_PLC_TESTS - Skip PLC-dependent testsCell_NestData[90].PartData.Member pathsUdtData struct with symbol_id and raw bytes
gArrayTest[0], gArrayTest[1], etc.Program:MainProgram.ArrayTest[0]๐ Major Milestone Achieved!
v0.6.0 introduces a new generic UDT format (UdtData) that works with any UDT without requiring prior knowledge of member structure. The library core is production-ready with all 31 unit tests passing.
The following operations are not supported due to PLC firmware restrictions. These limitations are inherent to the Allen-Bradley PLC firmware and cannot be bypassed at the library level.
Cannot write directly to STRING tags (e.g., gTest_STRING, Program:TestProgram.gTest_STRING).
Root Cause: PLC firmware limitation (CIP Error 0x2107). The PLC rejects direct write operations to STRING tags, regardless of the communication method used.
What Works:
gTest_STRING (read successfully)gTestUDT.Member5_String (read successfully)What Doesn't Work:
gTest_STRING (write fails - PLC limitation)Program:TestProgram.gTest_STRING (write fails - PLC limitation)gTestUDT.Member5_String (write fails - must write entire UDT)Workaround for STRING Members in UDTs: If the STRING is part of a UDT structure, you can write it by reading the entire UDT, modifying the STRING member in memory, then writing the entire UDT back:
// Read entire UDT
let mut udt = client.read_tag("gTestUDT").await?;
// Modify STRING member in memory (if UDT structure is known)
// ... modify UDT structure ...
// Write entire UDT back
client.write_tag("gTestUDT", udt).await?;
Note: For standalone STRING tags (not part of a UDT), there is no workaround at the communication library level. Alternative approaches may include using PLC ladder logic or other PLC-side mechanisms to update STRING values.
Cannot write directly to members of UDT array elements (e.g., gTestUDT_Array[0].Member1_DINT).
Root Cause: PLC firmware limitation (CIP Error 0x2107). The PLC does not support direct write operations to individual members within UDT array elements.
What Works:
gTestUDT_Array[0].Member1_DINT (read successfully)gTestUDT_Array[0] (write full UDT structure)gTestUDT.Member1_DINT (write individual members of non-array UDTs)gArray[5] (write elements of simple arrays like DINT[], REAL[], etc.)What Doesn't Work:
gTestUDT_Array[0].Member1_DINT (write fails - PLC limitation)Program:TestProgram.gTestUDT_Array[0].Member1_DINT (write fails - PLC limitation)Workaround: Use a read-modify-write pattern:
// Read entire UDT array element
let mut element = client.read_tag("gTestUDT_Array[0]").await?;
// Modify member in memory (if UDT structure is known)
// ... modify UDT structure ...
// Write entire UDT array element back
client.write_tag("gTestUDT_Array[0]", element).await?;
Test Results (392 tags tested):
gTestUDT_Array[0].Member1_DINT)gTest_STRING)gTestUDT.Member5_String)Important Notes:
๐ For detailed technical information about these limitations, including official Rockwell documentation references and technical background, see AB_String_UDT_Write_Limitations.md.
Program:MainProgram.Tag1 โ
FIXED in v0.5.4MyArray[5], MyArray[1,2,3] โ
WORKING in v0.5.5
MyDINT.15 (access individual bits)MyUDT.Member1.SubMemberMyString.LEN, MyString.DATA[5]Program:Production.Lines[2].Stations[5].Motor.Status.15All Allen-Bradley native data types with proper CIP encoding:
Complete C# wrapper with all data types
22 FFI functions for seamless integration
Type-safe API with comprehensive error handling
Cross-platform support (Windows, Linux, macOS)
Production-ready examples: WinForms, WPF, ASP.NET
Advanced features: TagGroup, Statistics, Batch Operations, STRING support
Status: Actively being polished to production quality
Experience a professional-grade HMI dashboard showcasing real-world industrial data tracking and monitoring capabilities. This demo demonstrates the library's potential for building production-ready SCADA systems and industrial dashboards.

This demo showcases:
The demo reads 13 industrial tags including machine status, production metrics, process parameters, and OEE data. See the ASP.NET example for complete tag specifications and setup instructions.
Optimized for PC applications with excellent performance:
๐ Latest Performance Improvements (v0.6.2)
Recent optimizations and improvements:
- Generic UDT Format: New
UdtDatastruct enables universal UDT handling- Memory allocation improvements: 20-30% reduction in allocation overhead for network operations
- Batch operations: 3-10x faster than individual operations
- Code quality: Enhanced with idiomatic Rust patterns and clippy optimizations
- Network efficiency: Optimized packet building with pre-allocated buffers
- Library Health: All 31 unit tests passing, production-ready core
| Operation | Throughput | Latency | Memory Usage |
|---|---|---|---|
| Single Tag Read | 3,000+ ops/sec | <1ms | ~800B |
| Single Tag Write | 1,500+ ops/sec | <2ms | ~800B |
| Batch Operations | 2,000+ ops/sec | 5-20ms | ~2KB |
| Real-time Subscriptions | 1,000+ tags/sec | 1-10ms | ~1KB |
| Tag Path Parsing | 10,000+ ops/sec | <0.1ms | ~1KB |
| Connection Setup | N/A | 50-200ms | ~4KB |
| Memory per Connection | N/A | N/A | ~4KB base |
UdtData struct (v0.6.0)Note: ControlLogix systems with CPUs in different slots can be tested using the RoutePath API.
Use EipClient::with_route_path() or set_route_path() with RoutePath::new().add_slot(slot_number)
to connect to ControlLogix CPUs in slots 0-31.
The easiest way to get started is by adding the crate to your Cargo.toml:
[dependencies]
```toml
[dependencies]
rust-ethernet-ip = "0.6.2"
tokio = { version = "1.0", features = ["full"] }
Install via NuGet:
<PackageReference Include="RustEtherNetIp" Version="0.6.2" />
Or via Package Manager Console:
Install-Package RustEtherNetIp
use rust_ethernet_ip::{EipClient, RoutePath};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to PLC
let mut client = EipClient::connect("192.168.0.1:44818").await?;
// Discover UDT structure automatically
let definition = client.get_udt_definition("Part_Data").await?;
println!("UDT: {}", definition.name);
for member in &definition.members {
println!(" {}: {} (offset: {}, size: {} bytes)",
member.name,
get_data_type_name(member.data_type),
member.offset,
member.size
);
}
// Read UDT data using discovered structure
let udt_data = client.read_udt_chunked("Part_Data").await?;
// Read individual members using discovered offsets
for member in &definition.members {
let value = client.read_udt_member_by_offset(
"Part_Data",
member.offset as usize,
member.size as usize,
member.data_type
).await?;
println!("{}: {:?}", member.name, value);
}
Ok(())
}
// Create route path for slot 2
let route = RoutePath::new()
.add_slot(0) // Backplane slot 0
.add_slot(2); // Target slot 2
// Connect with route path
let mut client = EipClient::with_route_path("192.168.0.1:44818", route).await?;
// Read tags through the route
let value = client.read_tag("TestTag").await?;
use rust_ethernet_ip::EipClient;
use std::net::SocketAddr;
use tokio::net::TcpStream;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a custom stream with socket options
let addr: SocketAddr = "192.168.1.100:44818".parse()?;
let stream = TcpStream::connect(addr).await?;
stream.set_nodelay(true)?;
stream.set_keepalive(true)?;
// Connect using the custom stream
let route = RoutePath::new().add_slot(0);
let mut client = EipClient::connect_with_stream(stream, Some(route)).await?;
// Use client normally
let value = client.read_tag("TestTag").await?;
Ok(())
}
Benefits:
use rust_ethernet_ip::{EipClient, PlcValue};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to CompactLogix PLC
let mut client = EipClient::connect("192.168.1.100:44818").await?;
// Read different data types
let motor_running = client.read_tag("Program:Main.MotorRunning").await?;
let production_count = client.read_tag("Program:Main.ProductionCount").await?;
let temperature = client.read_tag("Program:Main.Temperature").await?;
// Write values
client.write_tag("Program:Main.SetPoint", PlcValue::Dint(1500)).await?;
client.write_tag("Program:Main.StartButton", PlcValue::Bool(true)).await?;
println!("Motor running: {:?}", motor_running);
println!("Production count: {:?}", production_count);
println!("Temperature: {:?}", temperature);
Ok(())
}
using RustEtherNetIp;
using var client = new EtherNetIpClient();
if (client.Connect("192.168.1.100:44818"))
{
// Read different data types
bool motorRunning = client.ReadBool("Program:Main.MotorRunning");
int productionCount = client.ReadDint("Program:Main.ProductionCount");
float temperature = client.ReadReal("Program:Main.Temperature");
// Write values
client.WriteDint("Program:Main.SetPoint", 1500);
client.WriteBool("Program:Main.StartButton", true);
Console.WriteLine($"Motor running: {motorRunning}");
Console.WriteLine($"Production count: {productionCount}");
Console.WriteLine($"Temperature: {temperature:F1}ยฐC");
}
// Program-scoped tags
let value = client.read_tag("Program:MainProgram.Tag1").await?;
// Array elements (v0.5.5 - automatic workaround)
let array_element = client.read_tag("Program:Main.MyArray[5]").await?;
// Writing array elements
client.write_tag("gArrayTest[0]", PlcValue::Dint(100)).await?;
// BOOL arrays work too
let bool_value = client.read_tag("gArrayBoolTest[5]").await?;
client.write_tag("gArrayBoolTest[5]", PlcValue::Bool(true)).await?;
let multi_dim = client.read_tag("Program:Main.Matrix[1,2,3]").await?;
// Bit access
let bit_value = client.read_tag("Program:Main.StatusWord.15").await?;
// UDT members
let udt_member = client.read_tag("Program:Main.MotorData.Speed").await?;
let nested_udt = client.read_tag("Program:Main.Recipe.Step1.Temperature").await?;
// String operations
let string_length = client.read_tag("Program:Main.ProductName.LEN").await?;
let string_char = client.read_tag("Program:Main.ProductName.DATA[0]").await?;
// All supported data types
let bool_val = client.read_tag("BoolTag").await?; // BOOL
let sint_val = client.read_tag("SintTag").await?; // SINT (-128 to 127)
let int_val = client.read_tag("IntTag").await?; // INT (-32,768 to 32,767)
let dint_val = client.read_tag("DintTag").await?; // DINT (-2.1B to 2.1B)
let lint_val = client.read_tag("LintTag").await?; // LINT (64-bit signed)
let usint_val = client.read_tag("UsintTag").await?; // USINT (0 to 255)
let uint_val = client.read_tag("UintTag").await?; // UINT (0 to 65,535)
let udint_val = client.read_tag("UdintTag").await?; // UDINT (0 to 4.3B)
let ulint_val = client.read_tag("UlintTag").await?; // ULINT (64-bit unsigned)
let real_val = client.read_tag("RealTag").await?; // REAL (32-bit float)
let lreal_val = client.read_tag("LrealTag").await?; // LREAL (64-bit double)
let string_val = client.read_tag("StringTag").await?; // STRING
let udt_val = client.read_tag("UdtTag").await?; // UDT
Dramatically improve performance with batch operations that execute multiple read/write operations in a single network packet. Perfect for data acquisition, recipe management, and coordinated control scenarios.
use rust_ethernet_ip::{EipClient, BatchOperation, PlcValue};
// Read multiple tags in a single operation
let tags_to_read = vec![
"ProductionCount",
"Temperature_1",
"Temperature_2",
"Pressure_1",
"FlowRate",
];
let results = client.read_tags_batch(&tags_to_read).await?;
for (tag_name, result) in results {
match result {
Ok(value) => println!("๐ {}: {:?}", tag_name, value),
Err(error) => println!("โ {}: {}", tag_name, error),
}
}
// Write multiple tags in a single operation
let tags_to_write = vec![
("SetPoint_1", PlcValue::Real(75.5)),
("SetPoint_2", PlcValue::Real(80.0)),
("EnableFlag", PlcValue::Bool(true)),
("ProductionMode", PlcValue::Dint(2)),
("RecipeNumber", PlcValue::Dint(42)),
];
let results = client.write_tags_batch(&tags_to_write).await?;
for (tag_name, result) in results {
match result {
Ok(()) => println!("โ
{}: Write successful", tag_name),
Err(error) => println!("โ {}: {}", tag_name, error),
}
}
use rust_ethernet_ip::BatchOperation;
let operations = vec![
// Read current values
BatchOperation::Read { tag_name: "CurrentTemp".to_string() },
BatchOperation::Read { tag_name: "CurrentPressure".to_string() },
// Write new setpoints
BatchOperation::Write {
tag_name: "TempSetpoint".to_string(),
value: PlcValue::Real(78.5)
},
BatchOperation::Write {
tag_name: "PressureSetpoint".to_string(),
value: PlcValue::Real(15.2)
},
// Update control flags
BatchOperation::Write {
tag_name: "AutoModeEnabled".to_string(),
value: PlcValue::Bool(true)
},
];
let results = client.execute_batch(&operations).await?;
for result in results {
match result.operation {
BatchOperation::Read { tag_name } => {
match result.result {
Ok(Some(value)) => println!("๐ Read {}: {:?} ({}ฮผs)",
tag_name, value, result.execution_time_us),
Err(error) => println!("โ Read {}: {}", tag_name, error),
}
}
BatchOperation::Write { tag_name, .. } => {
match result.result {
Ok(_) => println!("โ
Write {}: Success ({}ฮผs)",
tag_name, result.execution_time_us),
Err(error) => println!("โ Write {}: {}", tag_name, error),
}
}
}
}
use rust_ethernet_ip::BatchConfig;
// High-performance configuration
let high_perf_config = BatchConfig {
max_operations_per_packet: 50, // More operations per packet
max_packet_size: 4000, // Larger packets for modern PLCs
packet_timeout_ms: 1000, // Faster timeout
continue_on_error: true, // Don't stop on single failures
optimize_packet_packing: true, // Optimize packet efficiency
};
client.configure_batch_operations(high_perf_config);
// Conservative/reliable configuration
let conservative_config = BatchConfig {
max_operations_per_packet: 10, // Fewer operations per packet
max_packet_size: 504, // Smaller packets for compatibility
packet_timeout_ms: 5000, // Longer timeout
continue_on_error: false, // Stop on first error
optimize_packet_packing: false, // Preserve exact operation order
};
client.configure_batch_operations(conservative_config);
use std::time::Instant;
let tags = vec!["Tag1", "Tag2", "Tag3", "Tag4", "Tag5"];
// Individual operations (traditional approach)
let individual_start = Instant::now();
for tag in &tags {
let _ = client.read_tag(tag).await?;
}
let individual_duration = individual_start.elapsed();
// Batch operations (optimized approach)
let batch_start = Instant::now();
let _ = client.read_tags_batch(&tags).await?;
let batch_duration = batch_start.elapsed();
let speedup = individual_duration.as_nanos() as f64 / batch_duration.as_nanos() as f64;
println!("๐ Performance improvement: {:.1}x faster with batch operations!", speedup);
// Batch operations provide detailed error information per operation
match client.execute_batch(&operations).await {
Ok(results) => {
let mut success_count = 0;
let mut error_count = 0;
for result in results {
match result.result {
Ok(_) => success_count += 1,
Err(_) => error_count += 1,
}
}
println!("๐ Results: {} successful, {} failed", success_count, error_count);
println!("๐ Success rate: {:.1}%",
(success_count as f32 / (success_count + error_count) as f32) * 100.0);
}
Err(e) => println!("โ Entire batch failed: {}", e),
}
# Windows
build.bat
# Linux/macOS
./build.sh
# Build Rust library
cargo build --release --lib
# Copy to C# project (Windows)
copy target\release\rust_ethernet_ip.dll csharp\RustEtherNetIp\
# Build C# wrapper
cd csharp/RustEtherNetIp
dotnet build --configuration Release
See BUILD.md for comprehensive build instructions.
Run the comprehensive test suite:
# Rust unit tests (30+ tests)
cargo test
# C# wrapper tests
cd csharp/RustEtherNetIp.Tests
dotnet test
# Run examples
cargo run --example advanced_tag_addressing
cargo run --example data_types_showcase
Explore comprehensive examples demonstrating all library capabilities:
Rich desktop application with MVVM architecture and modern UI.

cd examples/WpfExample
dotnet run
Features:
Perfect for: Desktop HMIs, engineering tools, maintenance applications
Traditional Windows Forms application with familiar UI patterns.

cd examples/WinFormsExample
dotnet run
Features:
Perfect for: Legacy system integration, simple HMIs, maintenance tools
RESTful API backend providing HTTP access to PLC functionality.

cd examples/AspNetExample
dotnet run
Features:
Perfect for: Web services, microservices, system integration, mobile backends
Native Rust examples demonstrating core library functionality.
# Advanced tag addressing showcase
cargo run --example advanced_tag_addressing
# Complete data types demonstration
cargo run --example data_types_showcase
# Batch operations performance demo
cargo run --example batch_operations_demo
# Stream injection example (v0.6.2)
cargo run --example stream_injection_example
# Nested UDT array test
cargo run --example test_cell_nestdata_udt
Features:
Perfect for: Rust applications, embedded systems, high-performance scenarios
Choose your platform:
Start the backend (for web examples):
cd examples/AspNetExample
dotnet run
Run your chosen example and connect to your PLC at 192.168.0.1:44818
Explore features:
examples/
โโโ WpfExample/ # WPF desktop application
โโโ WinFormsExample/ # WinForms desktop application
โโโ AspNetExample/ # ASP.NET Core Web API
โโโ rust_examples/ # Native Rust examples
โ โโโ advanced_tag_addressing.rs
โ โโโ data_types_showcase.rs
โ โโโ batch_operations_demo.rs
โโโ csharp_examples/ # Additional C# examples
Each example includes comprehensive documentation, setup instructions, and demonstrates different aspects of the library's capabilities.
This project is developed for the industrial automation community. If you find this library valuable for your projects, please consider sponsoring its development!
We value your input! Help us improve the library by sharing:
Experiencing issues? Check out our comprehensive troubleshooting guide:
๐ Complete Troubleshooting Guide
| Error Code | Meaning | Quick Fix |
|---|---|---|
| 0x01 | Connection failure | Check tag name, scope, and External Access permissions |
| 0x04 | Path segment error | Verify tag path format (controller vs program-scoped) |
| 0x05 | Path destination unknown | Check ControlLogix slot routing |
| 0x16 | Object does not exist | Verify tag exists and is downloaded to PLC |
1. CIP Error 0x01: Connection Failure
"Program:ProgramName.TagName"2. Tag Not Found
discover_tags() to find available tags3. ControlLogix Routing Issues
let route = RoutePath::new().add_slot(3); // CPU in slot 3
let mut client = EipClient::with_route_path("192.168.1.100:44818", route).await?;
4. Connection Timeout
5. Nested UDT Array Members (v0.6.2)
Cell_NestData[90].PartData.Member are now fully supportedTagPath::parse() for paths with member access after array brackets6. Testing Without PLC
SKIP_PLC_TESTS=1 environment variable to skip PLC-dependent testsTEST_PLC_ADDRESS to your PLC IP for integration teststests/README.md for complete test configuration guideFor detailed troubleshooting steps, code examples, and debugging procedures, see the Complete Troubleshooting Guide.
This project draws inspiration from excellent libraries in the industrial automation space:
We welcome contributions! Please see our Contributing Guide for details on:
This library is provided "AS IS" without warranty of any kind. Users assume full responsibility for its use in their applications and systems.
The developers and contributors make NO WARRANTIES, EXPRESS OR IMPLIED, including but not limited to:
Under no circumstances shall the developers, contributors, or associated parties be liable for:
By using this library, you acknowledge and agree that:
Users agree to indemnify and hold harmless the developers and contributors from any claims, damages, or liabilities arising from the use of this library.
โ ๏ธ IMPORTANT: This disclaimer is an integral part of the license terms. Use of this library constitutes acceptance of these terms.
This project is licensed under the MIT License - see the LICENSE file for details.
Built for the industrial automation community
To build all libraries and examples:
./build-all.bat
This script builds:
See BUILD.md for details.
Current Release: v0.6.1 (CHANGELOG.md)
connect_with_stream() for custom TCP transportUdtData struct with symbol_id and raw bytesSee CHANGELOG.md for a full list of changes.
See RELEASE_NOTES_v0.5.0.md for detailed release notes and migration info.