pub struct EipClient { /* private fields */ }Expand description
High-performance EtherNet/IP client for PLC communication
This struct provides the core functionality for communicating with Allen-Bradley PLCs using the EtherNet/IP protocol. It handles connection management, session registration, and tag operations.
§Thread Safety
The EipClient is NOT thread-safe. For multi-threaded applications:
use std::sync::Arc;
use tokio::sync::Mutex;
use rust_ethernet_ip::EipClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Create a thread-safe wrapper
let client = Arc::new(Mutex::new(EipClient::connect("192.168.1.100:44818").await?));
// Use in multiple threads
let client_clone = client.clone();
tokio::spawn(async move {
let mut client = client_clone.lock().await;
let _ = client.read_tag("Tag1").await?;
Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
});
Ok(())
}§Performance Characteristics
| Operation | Latency | Throughput | Memory |
|---|---|---|---|
| Connect | 100-500ms | N/A | ~8KB |
| Read Tag | 1-5ms | 1,500+ ops/sec | ~2KB |
| Write Tag | 2-10ms | 600+ ops/sec | ~2KB |
| Batch Read | 5-20ms | 2,000+ ops/sec | ~4KB |
§Known Limitations
The following nested write paths have controller-specific caveats:
§UDT Array Element Member Writes
Historical tests classified direct writes to UDT array element members
(e.g., gTestUDT_Array[0].Member1_DINT) as firmware-blocked 0x2107
cases. CODEX-AM and CODEX-AV disproved the blanket rule on a
5069-L330ERM fw38: scalar DINT/REAL/BOOL/INT member writes succeeded with
corrected paths, verified read-back, preserved sibling members, and restored
cleanly.
§STRING Tags and STRING Members in UDTs
Standalone Logix STRING tags can be written directly with the standard structure
encoding. Direct writes to STRING members within UDTs (e.g.,
gTestUDT.Member5_String) are still rejected with 0xFF/0x2107 under the
current member encoding on 5069-L330ERM fw38. Whether a member-specific direct
encoding exists remains under CODEX-AO investigation. For those nested members,
write the entire UDT structure instead of writing the member path directly.
What works:
- ✅ Reading UDT array element members:
gTestUDT_Array[0].Member1_DINT(read) - ✅ Writing entire UDT array elements:
gTestUDT_Array[0](write full UDT) - ✅ Writing UDT members (non-STRING):
gTestUDT.Member1_DINT(write DINT/REAL/BOOL/INT members) - ✅ Writing scalar UDT array element members on 5069-L330ERM fw38 with corrected paths
- ✅ Writing array elements:
gArray[5](write element of simple array) - ✅ Reading STRING tags:
gTest_STRING(read) - ✅ Writing STRING tags:
gTest_STRING(write standard top-level STRING) - ✅ Reading STRING members in UDTs:
gTestUDT.Member5_String(read)
What doesn’t work:
- ❌ Writing STRING members in UDTs:
gTestUDT.Member5_String(write) - must write entire UDT - ❌ Writing program-scoped STRING members:
Program:TestProgram.gTestUDT.Member5_String(write) - must write entire UDT - ❌ Writing UDT array element STRING members under the current member encoding - must write entire UDT
Conservative workaround: For rejected nested STRING-member writes, read the entire UDT array element, modify the member in memory, then write the entire UDT array element back:
use rust_ethernet_ip::{PlcValue, UdtData};
// Read the entire UDT array element
let udt_value = client.read_tag("gTestUDT_Array[0]").await?;
if let PlcValue::Udt(mut udt_data) = udt_value {
let udt_def = client.get_udt_definition("gTestUDT_Array").await?;
// Convert UdtDefinition to UserDefinedType
let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
for member in &udt_def.members {
user_def.add_member(member.clone());
}
let mut members = udt_data.parse(&user_def)?;
// Modify the member
members.insert("Member1_DINT".to_string(), PlcValue::Dint(100));
// Write the entire UDT array element back
let modified_udt = UdtData::from_hash_map(&members, &user_def, udt_data.symbol_id)?;
client.write_tag("gTestUDT_Array[0]", PlcValue::Udt(modified_udt)).await?;
}§Error Handling
All operations return Result<T, EtherNetIpError>. Common errors include:
use rust_ethernet_ip::{EipClient, EtherNetIpError};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = EipClient::connect("192.168.1.100:44818").await?;
match client.read_tag("Tag1").await {
Ok(value) => println!("Tag value: {:?}", value),
Err(EtherNetIpError::Protocol(_)) => println!("Tag does not exist"),
Err(EtherNetIpError::Connection(_)) => println!("Lost connection to PLC"),
Err(EtherNetIpError::Timeout(_)) => println!("Operation timed out"),
Err(e) => println!("Other error: {}", e),
}
Ok(())
}§Examples
Basic usage:
use rust_ethernet_ip::{EipClient, PlcValue};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = EipClient::connect("192.168.1.100:44818").await?;
// Read a boolean tag
let motor_running = client.read_tag("MotorRunning").await?;
// Write an integer tag
client.write_tag("SetPoint", PlcValue::Dint(1500)).await?;
// Read multiple tags in sequence
let tag1 = client.read_tag("Tag1").await?;
let tag2 = client.read_tag("Tag2").await?;
let tag3 = client.read_tag("Tag3").await?;
Ok(())
}Advanced usage with error recovery:
use rust_ethernet_ip::{EipClient, PlcValue, EtherNetIpError};
use tokio::time::Duration;
async fn read_with_retry(client: &mut EipClient, tag: &str, retries: u32) -> Result<PlcValue, EtherNetIpError> {
for attempt in 0..retries {
match client.read_tag(tag).await {
Ok(value) => return Ok(value),
Err(EtherNetIpError::Connection(_)) => {
if attempt < retries - 1 {
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
return Err(EtherNetIpError::Protocol("Max retries exceeded".to_string()));
}
Err(e) => return Err(e),
}
}
Err(EtherNetIpError::Protocol("Max retries exceeded".to_string()))
}Implementations§
Source§impl EipClient
impl EipClient
Sourcepub async fn execute_batch(
&mut self,
operations: &[BatchOperation],
) -> Result<Vec<BatchResult>>
pub async fn execute_batch( &mut self, operations: &[BatchOperation], ) -> Result<Vec<BatchResult>>
Executes a batch of read and write operations
This is the main entry point for batch operations. It takes a slice of
BatchOperation items and executes them efficiently by grouping them
into optimal CIP packets based on the current BatchConfig.
§Arguments
operations- A slice of operations to execute
§Returns
A vector of BatchResult items, one per executed operation.
When optimize_packet_packing is enabled, operations may be regrouped
by type for execution, so result order is not guaranteed to match the
original mixed-operation input order. Use BatchResult::operation to
correlate each result.
§Performance
Batch execution primarily reduces round trips by combining multiple operations into fewer requests. Observed throughput varies significantly between simulator and real hardware, and also depends on packet sizing, controller model, route path, and tag mix.
§Examples
use rust_ethernet_ip::{EipClient, BatchOperation, PlcValue};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = EipClient::connect("192.168.1.100:44818").await?;
let operations = vec![
BatchOperation::Read { tag_name: "Motor1_Speed".to_string() },
BatchOperation::Read { tag_name: "Motor2_Speed".to_string() },
BatchOperation::Write {
tag_name: "SetPoint".to_string(),
value: PlcValue::Dint(1500)
},
];
let results = client.execute_batch(&operations).await?;
for result in results {
match result.result {
Ok(Some(value)) => println!("Read value: {:?}", value),
Ok(None) => println!("Write successful"),
Err(e) => println!("Operation failed: {}", e),
}
}
Ok(())
}Reads multiple tags in a single batch operation
This is a convenience method for read-only batch operations. It’s optimized for reading many tags at once.
§Arguments
tag_names- A slice of tag names to read
§Returns
A vector of tuples containing (tag_name, result) pairs
§Examples
use rust_ethernet_ip::EipClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = EipClient::connect("192.168.1.100:44818").await?;
let tags = ["Motor1_Speed", "Motor2_Speed", "Temperature", "Pressure"];
let results = client.read_tags_batch(&tags).await?;
for (tag_name, result) in results {
match result {
Ok(value) => println!("{}: {:?}", tag_name, value),
Err(e) => println!("{}: Error - {}", tag_name, e),
}
}
Ok(())
}Writes multiple tag values in a single batch operation
This is a convenience method for write-only batch operations. It’s optimized for writing many values at once.
§Arguments
tag_values- A slice of(tag_name, value)tuples to write
§Returns
A vector of tuples containing (tag_name, result) pairs
§Examples
use rust_ethernet_ip::{EipClient, PlcValue};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = EipClient::connect("192.168.1.100:44818").await?;
let writes = vec![
("SetPoint1", PlcValue::Bool(true)),
("SetPoint2", PlcValue::Dint(2000)),
("EnableFlag", PlcValue::Bool(true)),
];
let results = client.write_tags_batch(&writes).await?;
for (tag_name, result) in results {
match result {
Ok(_) => println!("{}: Write successful", tag_name),
Err(e) => println!("{}: Write failed - {}", tag_name, e),
}
}
Ok(())
}Sourcepub fn configure_batch_operations(&mut self, config: BatchConfig)
pub fn configure_batch_operations(&mut self, config: BatchConfig)
Configures batch operation settings
This method allows fine-tuning of batch operation behavior, including performance optimizations and error handling.
§Arguments
config- The new batch configuration to use
§Examples
use rust_ethernet_ip::{EipClient, BatchConfig};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = EipClient::connect("192.168.1.100:44818").await?;
let config = BatchConfig {
max_operations_per_packet: 50,
max_packet_size: 1500,
packet_timeout_ms: 5000,
continue_on_error: false,
optimize_packet_packing: true,
};
client.configure_batch_operations(config);
Ok(())
}Sourcepub fn get_batch_config(&self) -> &BatchConfig
pub fn get_batch_config(&self) -> &BatchConfig
Gets current batch operation configuration
Source§impl EipClient
impl EipClient
Sourcepub async fn check_health(&self) -> bool
pub async fn check_health(&self) -> bool
Quick connection health check (no I/O).
Returns true if the session handle is valid and there has been activity
within the last 150 seconds. Use this for cheap periodic checks; for a
definitive check that the PLC is still responding, use check_health_detailed.
Sourcepub async fn get_diagnostics_snapshot(&self) -> DiagnosticsSnapshot
pub async fn get_diagnostics_snapshot(&self) -> DiagnosticsSnapshot
Builds a lightweight diagnostics snapshot from the current client state.
Sourcepub async fn check_health_detailed(&mut self) -> Result<bool>
pub async fn check_health_detailed(&mut self) -> Result<bool>
Verifies the connection by sending a keep-alive (and re-registering if needed).
Use this when you need to confirm the PLC is still reachable (e.g. after
a long idle or before a critical operation). On failure, consider
reconnecting; check EtherNetIpError::is_retriable on errors.
Sourcepub async fn get_diagnostics_snapshot_detailed(
&mut self,
) -> Result<DiagnosticsSnapshot>
pub async fn get_diagnostics_snapshot_detailed( &mut self, ) -> Result<DiagnosticsSnapshot>
Builds a verified diagnostics snapshot by actively checking PLC connectivity.
Source§impl EipClient
impl EipClient
Sourcepub async fn export_schema(&mut self) -> Result<SchemaExport>
pub async fn export_schema(&mut self) -> Result<SchemaExport>
Exports a stable schema view built from the current discovery APIs.
Sourcepub async fn export_schema_json(&mut self) -> Result<String>
pub async fn export_schema_json(&mut self) -> Result<String>
Exports the current schema view as pretty-printed JSON.
Source§impl EipClient
impl EipClient
pub async fn write_string_tag( &mut self, tag_name: &str, value: &str, ) -> Result<()>
Sourcepub async fn read_string_tag(&mut self, tag_name: &str) -> Result<String>
pub async fn read_string_tag(&mut self, tag_name: &str) -> Result<String>
Reads a Logix STRING tag as text, whether it is the built-in STRING type or a custom
string type (own name/length). read_tag decodes only the built-in handle (0x0FCE) to
PlcValue::String and returns any other structure as PlcValue::Udt; here the caller has
asserted the tag is a string, so a structure payload is decoded as [handle][LEN][DATA].
pub async fn write_udt_member( &mut self, udt_tag_name: &str, member_name: &str, value: PlcValue, ) -> Result<()>
pub async fn write_udt_array_member( &mut self, udt_array_element_path: &str, member_name: &str, value: PlcValue, ) -> Result<()>
Source§impl EipClient
impl EipClient
Sourcepub async fn write_ab_string_components(
&mut self,
_tag_name: &str,
_value: &str,
) -> Result<()>
👎Deprecated since 1.2.0: never provided a safe atomic STRING write contract; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
pub async fn write_ab_string_components( &mut self, _tag_name: &str, _value: &str, ) -> Result<()>
never provided a safe atomic STRING write contract; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
Retired non-atomic AB STRING component writer.
Sourcepub async fn write_ab_string_udt(
&mut self,
_tag_name: &str,
_value: &str,
) -> Result<()>
👎Deprecated since 1.2.0: never parsed the PLC reply correctly and could report false success; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
pub async fn write_ab_string_udt( &mut self, _tag_name: &str, _value: &str, ) -> Result<()>
never parsed the PLC reply correctly and could report false success; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
Retired malformed AB STRING-as-UDT writer.
Sourcepub async fn write_string_connected(
&mut self,
_tag_name: &str,
_value: &str,
) -> Result<()>
👎Deprecated since 1.2.0: connected STRING messaging never established a valid Forward Open path; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
pub async fn write_string_connected( &mut self, _tag_name: &str, _value: &str, ) -> Result<()>
connected STRING messaging never established a valid Forward Open path; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
Retired connected explicit-messaging STRING writer.
Sourcepub async fn write_string_unconnected(
&mut self,
_tag_name: &str,
_value: &str,
) -> Result<()>
👎Deprecated since 1.2.0: malformed exploratory STRING payload; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
pub async fn write_string_unconnected( &mut self, _tag_name: &str, _value: &str, ) -> Result<()>
malformed exploratory STRING payload; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
Retired reverse-engineered unconnected STRING writer.
Sourcepub async fn write_string(
&mut self,
_tag_name: &str,
_value: &str,
) -> Result<()>
👎Deprecated since 1.2.0: legacy malformed STRING writer; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
pub async fn write_string( &mut self, _tag_name: &str, _value: &str, ) -> Result<()>
legacy malformed STRING writer; use write_tag(…, PlcValue::String(…)) or write_string_tag; removal planned for 2.0
Retired legacy STRING writer.
Use EipClient::write_tag with crate::PlcValue::String or
EipClient::write_string_tag. Those paths use the hardware-validated
standard Logix STRING structure encoding.
Source§impl EipClient
impl EipClient
Sourcepub async fn subscribe_to_tag(
&self,
tag_path: &str,
options: SubscriptionOptions,
) -> Result<TagSubscription>
pub async fn subscribe_to_tag( &self, tag_path: &str, options: SubscriptionOptions, ) -> Result<TagSubscription>
Subscribes to a tag for real-time updates.
The returned TagSubscription can be used to:
wait_for_update()for the next valueget_last_value()for the latest cached valueinto_stream()for an asyncStreamof updates
This API validates the tag with an initial read before returning so invalid or inaccessible tags fail fast instead of surfacing only through background polling logs.
The background poll loop uses SubscriptionOptions::update_rate (milliseconds) between reads.
Sourcepub async fn unsubscribe(&self, tag_path: &str) -> bool
pub async fn unsubscribe(&self, tag_path: &str) -> bool
Stops and removes subscriptions for a tag path.
Returns true when at least one subscription was found. Polling stops at the next loop check, normally within one configured update interval.
Subscribes to multiple tags. Returns one TagSubscription per tag in order.
Sourcepub async fn upsert_tag_group(
&self,
group_name: &str,
tags: &[&str],
update_rate_ms: u32,
) -> Result<()>
pub async fn upsert_tag_group( &self, group_name: &str, tags: &[&str], update_rate_ms: u32, ) -> Result<()>
Registers or replaces a named tag group for grouped polling.
Tag groups are useful for HMI/SCADA-style scan classes where multiple tags share a polling interval and should be read together.
Sourcepub async fn remove_tag_group(&self, group_name: &str) -> bool
pub async fn remove_tag_group(&self, group_name: &str) -> bool
Removes a named tag group.
Sourcepub async fn list_tag_groups(&self) -> Vec<TagGroupConfig>
pub async fn list_tag_groups(&self) -> Vec<TagGroupConfig>
Lists all currently registered tag groups.
Sourcepub async fn read_tag_group_once(
&self,
group_name: &str,
) -> Result<TagGroupSnapshot>
pub async fn read_tag_group_once( &self, group_name: &str, ) -> Result<TagGroupSnapshot>
Reads all tags in a group once and returns a snapshot.
Sourcepub async fn subscribe_tag_group(
&self,
group_name: &str,
) -> Result<TagGroupSubscription>
pub async fn subscribe_tag_group( &self, group_name: &str, ) -> Result<TagGroupSubscription>
Starts background polling for a registered tag group.
Use the returned subscription to await snapshots and to stop polling.
Source§impl EipClient
impl EipClient
pub async fn new(addr: &str) -> Result<Self>
Sourcepub fn set_max_packet_size(&mut self, size: u32)
pub fn set_max_packet_size(&mut self, size: u32)
Sets the maximum packet size for communication
Discovers all tags in the PLC (including hierarchical UDT members)
Sourcepub async fn discover_udt_members(
&mut self,
udt_name: &str,
) -> Result<Vec<(String, TagMetadata)>>
pub async fn discover_udt_members( &mut self, udt_name: &str, ) -> Result<Vec<(String, TagMetadata)>>
Discovers UDT members for a specific structure
Sourcepub async fn get_udt_definition_cached(
&self,
udt_name: &str,
) -> Option<UdtDefinition>
pub async fn get_udt_definition_cached( &self, udt_name: &str, ) -> Option<UdtDefinition>
Gets cached UDT definition
Sourcepub async fn list_udt_definitions(&self) -> Vec<String>
pub async fn list_udt_definitions(&self) -> Vec<String>
Lists all cached UDT definitions
Discovers all tags with full attributes This method queries the PLC for all available tags and their detailed attributes
Discovers program-scoped tags This method discovers tags within a specific program scope
Sourcepub async fn list_cached_tag_attributes(&self) -> Vec<String>
pub async fn list_cached_tag_attributes(&self) -> Vec<String>
Lists all cached tag attributes
Sourcepub async fn clear_caches(&mut self)
pub async fn clear_caches(&mut self)
Clears cached tag metadata and UDT-related caches.
Sourcepub async fn with_route_path(addr: &str, route: RoutePath) -> Result<Self>
pub async fn with_route_path(addr: &str, route: RoutePath) -> Result<Self>
Creates a new client with a specific route path
Sourcepub async fn connect_with_stream<S>(
stream: S,
route: Option<RoutePath>,
) -> Result<Self>where
S: EtherNetIpStream + 'static,
pub async fn connect_with_stream<S>(
stream: S,
route: Option<RoutePath>,
) -> Result<Self>where
S: EtherNetIpStream + 'static,
Connect to a PLC using a custom stream
This method allows you to provide your own stream implementation, enabling:
- Wrapping streams for metrics/observability (bytes in/out)
- Applying custom socket options (keepalive, timeouts, bind local address)
- Reusing pre-established tunnels/connections
- Using in-memory streams for deterministic testing
§Arguments
stream- Any stream that implementsAsyncRead + AsyncWrite + Unpin + Send
§Example
use rust_ethernet_ip::EipClient;
use std::io::Cursor;
// Any AsyncRead + AsyncWrite + Unpin + Send stream can be injected.
let stream = Cursor::new(Vec::<u8>::new());
// Connect using the custom stream
let client = EipClient::connect_with_stream(stream, None).await?;Sourcepub fn set_route_path(&mut self, route: RoutePath)
pub fn set_route_path(&mut self, route: RoutePath)
Sets the route path for the client
Sourcepub fn get_route_path(&self) -> Option<RoutePath>
pub fn get_route_path(&self) -> Option<RoutePath>
Gets the current route path
Sourcepub fn clear_route_path(&mut self)
pub fn clear_route_path(&mut self)
Removes the route path (uses direct connection)
Sourcepub async fn get_tag_metadata(&self, tag_name: &str) -> Option<TagMetadata>
pub async fn get_tag_metadata(&self, tag_name: &str) -> Option<TagMetadata>
Gets metadata for a tag
Sourcepub async fn read_tag(&mut self, tag_name: &str) -> Result<PlcValue>
pub async fn read_tag(&mut self, tag_name: &str) -> Result<PlcValue>
Reads a tag value from the PLC
This function performs a CIP read request for the specified tag. The tag’s data type is automatically determined from the PLC’s response.
v0.6.0: For UDT tags, this returns PlcValue::Udt(UdtData) with symbol_id
and raw bytes. Use UdtData::parse() with a UDT definition to access members.
§Arguments
tag_name- The name of the tag to read
§Returns
The tag’s value as a PlcValue enum. For UDTs, this is PlcValue::Udt(UdtData).
§Examples
use rust_ethernet_ip::{EipClient, PlcValue};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut client = EipClient::connect("192.168.1.100:44818").await?;
// Read different data types
let bool_val = client.read_tag("MotorRunning").await?;
let int_val = client.read_tag("Counter").await?;
let real_val = client.read_tag("Temperature").await?;
// Read a UDT (v0.6.0: returns UdtData)
let udt_val = client.read_tag("MyUDT").await?;
if let PlcValue::Udt(udt_data) = udt_val {
let udt_def = client.get_udt_definition("MyUDT").await?;
// Convert UdtDefinition to UserDefinedType
let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
for member in &udt_def.members {
user_def.add_member(member.clone());
}
let members = udt_data.parse(&user_def)?;
println!("UDT has {} members", members.len());
}
// Handle the result
match bool_val {
PlcValue::Bool(true) => println!("Motor is running"),
PlcValue::Bool(false) => println!("Motor is stopped"),
_ => println!("Unexpected data type"),
}
Ok(())
}§Performance
- Latency: 1-5ms typical
- Throughput: 1,500+ ops/sec
- Network: 1 request/response cycle
§Error Handling
Common errors:
Protocol: Tag doesn’t exist or invalid formatConnection: Lost connection to PLCTimeout: Operation timed out
Sourcepub async fn write_bit(
&mut self,
tag_base: &str,
bit_index: u8,
value: bool,
) -> Result<()>
pub async fn write_bit( &mut self, tag_base: &str, bit_index: u8, value: bool, ) -> Result<()>
Sourcepub async fn read_array_range(
&mut self,
base_array_name: &str,
start_index: u32,
element_count: u32,
) -> Result<Vec<PlcValue>>
pub async fn read_array_range( &mut self, base_array_name: &str, start_index: u32, element_count: u32, ) -> Result<Vec<PlcValue>>
Read a range of elements from a basic-type PLC array.
This method reads arrays in chunks under the hood to avoid PLC packet-size limits. It supports basic CIP scalar types: BOOL, SINT, INT, DINT, LINT, USINT, UINT, UDINT, ULINT, REAL, LREAL.
§Arguments
base_array_name- Base array tag name without index (e.g.,"MyDintArray")start_index- Starting element indexelement_count- Number of elements to read
§Returns
A Vec<PlcValue> with one element per requested array entry.
Sourcepub fn build_write_array_request_with_index(
&self,
base_array_name: &str,
start_index: u32,
element_count: u16,
data_type: u16,
data: &[u8],
) -> Result<Vec<u8>>
pub fn build_write_array_request_with_index( &self, base_array_name: &str, start_index: u32, element_count: u16, data_type: u16, data: &[u8], ) -> Result<Vec<u8>>
Builds a CIP Write Tag Service request for array elements with element addressing
This method uses proper CIP element addressing (0x28/0x29/0x2A segments) in the Request Path to write to specific array elements or ranges.
Reference: 1756-PM020, Pages 603-611, 855-867 (Writing to Array Element)
§Arguments
base_array_name- Base name of the array (e.g.,"MyArray"for"MyArray[10]")start_index- Starting element index (0-based)element_count- Number of elements to writedata_type- CIP data type code (e.g., 0x00C4 for DINT)data- Raw bytes of the data to write
§Example
Writing value 0x12345678 to element 10 of array “MyArray”:
let data = 0x12345678u32.to_le_bytes();
let request = client.build_write_array_request_with_index(
"MyArray", 10, 1, 0x00C4, &data
)?;Sourcepub async fn read_udt_chunked(&mut self, tag_name: &str) -> Result<PlcValue>
pub async fn read_udt_chunked(&mut self, tag_name: &str) -> Result<PlcValue>
Reads a UDT through the maintained normal tag-read path.
v0.6.0: Returns PlcValue::Udt(UdtData) with symbol_id and raw bytes.
Use UdtData::parse() with a UDT definition to access individual members.
The historical “advanced chunked” strategy ladder was retired in 1.2.0
because its alternate request builders were protocol-invalid and could
convert failures into fabricated empty UDT payloads. This compatibility
method now delegates to read_tag and returns an error if the target is
not decoded as a UDT. Correct fragmented UDT reads belong to the
capture-gated CODEX-AO wire-format work.
§Arguments
tag_name- The name of the UDT tag to read
§Returns
PlcValue::Udt(UdtData) containing the symbol_id and raw data bytes
§Example
let udt_value = client.read_udt_chunked("Part_Data").await?;
if let rust_ethernet_ip::PlcValue::Udt(udt_data) = udt_value {
println!("UDT symbol_id: {}, data size: {} bytes", udt_data.symbol_id, udt_data.data.len());
// Parse members if needed
let udt_def = client.get_udt_definition("Part_Data").await?;
// Convert UdtDefinition to UserDefinedType
let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
for member in &udt_def.members {
user_def.add_member(member.clone());
}
let members = udt_data.parse(&user_def)?;
}Sourcepub async fn read_udt_member_by_offset(
&mut self,
_udt_name: &str,
_member_offset: usize,
_member_size: usize,
_data_type: u16,
) -> Result<PlcValue>
👎Deprecated since 1.2.0: offset-based UDT member access indexed the CIP envelope, not the UDT payload; use read_udt_chunked + UdtData::parse or direct member tag reads; removal planned for 2.0
pub async fn read_udt_member_by_offset( &mut self, _udt_name: &str, _member_offset: usize, _member_size: usize, _data_type: u16, ) -> Result<PlcValue>
offset-based UDT member access indexed the CIP envelope, not the UDT payload; use read_udt_chunked + UdtData::parse or direct member tag reads; removal planned for 2.0
Reads a specific UDT member by offset
This method reads a specific member of a UDT by calculating its offset and reading only that portion of the UDT.
§Arguments
udt_name- The name of the UDT tagmember_offset- The byte offset of the member in the UDTmember_size- The size of the member in bytesdata_type- The data type of the member (0x00C1 for BOOL, 0x00CA for REAL, etc.)
§Example
let member_value = client.read_udt_member_by_offset("MyUDT", 0, 1, 0x00C1).await?;
println!("Member value: {:?}", member_value);Sourcepub async fn write_udt_member_by_offset(
&mut self,
_udt_name: &str,
_member_offset: usize,
_member_size: usize,
_data_type: u16,
_value: PlcValue,
) -> Result<()>
👎Deprecated since 1.2.0: offset-based UDT member writes round-tripped CIP envelope bytes as tag data; use write_udt_member/write_udt_array_member or direct member tag writes; removal planned for 2.0
pub async fn write_udt_member_by_offset( &mut self, _udt_name: &str, _member_offset: usize, _member_size: usize, _data_type: u16, _value: PlcValue, ) -> Result<()>
offset-based UDT member writes round-tripped CIP envelope bytes as tag data; use write_udt_member/write_udt_array_member or direct member tag writes; removal planned for 2.0
Writes a specific UDT member by offset
This method writes a specific member of a UDT by calculating its offset and writing only that portion of the UDT.
§Arguments
udt_name- The name of the UDT tagmember_offset- The byte offset of the member in the UDTmember_size- The size of the member in bytesdata_type- The data type of the member (0x00C1 for BOOL, 0x00CA for REAL, etc.)value- The value to write
§Example
client.write_udt_member_by_offset("MyUDT", 0, 1, 0x00C1, PlcValue::Bool(true)).await?;Sourcepub async fn get_udt_definition(
&mut self,
udt_name: &str,
) -> Result<UdtDefinition>
pub async fn get_udt_definition( &mut self, udt_name: &str, ) -> Result<UdtDefinition>
Gets UDT definition from the PLC This method queries the PLC for the UDT structure and caches it for future use
Sourcepub async fn get_tag_attributes(
&mut self,
tag_name: &str,
) -> Result<TagAttributes>
pub async fn get_tag_attributes( &mut self, tag_name: &str, ) -> Result<TagAttributes>
Gets tag attributes (type, size, dimensions, scope) from the PLC.
Use this to introspect a tag before reading or writing: discover data type, size in bytes, array dimensions, and scope (controller vs program). Results are cached per tag for the lifetime of the client.
§Example
let attrs = client.get_tag_attributes("MyTag").await?;
println!("Type: {}, size: {} bytes", attrs.data_type_name, attrs.size);
if !attrs.dimensions.is_empty() {
println!("Array dimensions: {:?}", attrs.dimensions);
}Sourcepub async fn write_tag(&mut self, tag_name: &str, value: PlcValue) -> Result<()>
pub async fn write_tag(&mut self, tag_name: &str, value: PlcValue) -> Result<()>
Writes a value to a PLC tag
This method automatically determines the best communication method based on the data type:
- STRING values use unconnected explicit messaging with proper AB STRING format
- Other data types use standard unconnected messaging
v0.6.0: For UDT tags, pass PlcValue::Udt(UdtData). The symbol_id must be set
(typically obtained by reading the UDT first). If symbol_id is 0, the method will
attempt to read tag attributes to get the symbol_id automatically.
§Arguments
tag_name- The name of the tag to write tovalue- The value to write. For UDTs, usePlcValue::Udt(UdtData).
§Example
use rust_ethernet_ip::{PlcValue, UdtData};
// Write simple types
client.write_tag("Counter", PlcValue::Dint(42)).await?;
client.write_tag("Message", PlcValue::String("Hello PLC".to_string())).await?;
// Write UDT (v0.6.0: read first to get symbol_id, then modify and write)
let udt_value = client.read_tag("MyUDT").await?;
if let PlcValue::Udt(mut udt_data) = udt_value {
let udt_def = client.get_udt_definition("MyUDT").await?;
// Convert UdtDefinition to UserDefinedType
let mut user_def = rust_ethernet_ip::udt::UserDefinedType::new(udt_def.name.clone());
for member in &udt_def.members {
user_def.add_member(member.clone());
}
let mut members = udt_data.parse(&user_def)?;
members.insert("Member1".to_string(), PlcValue::Dint(100));
let modified_udt = UdtData::from_hash_map(&members, &user_def, udt_data.symbol_id)?;
client.write_tag("MyUDT", PlcValue::Udt(modified_udt)).await?;
}Sourcepub async fn send_cip_request(&self, cip_request: &[u8]) -> Result<Vec<u8>>
pub async fn send_cip_request(&self, cip_request: &[u8]) -> Result<Vec<u8>>
Sends a CIP request using EtherNet/IP SendRRData.
Primary mode uses Unconnected Send (0x52) wrapping. For controllers that reject this pattern for specific services, a direct-CIP fallback is attempted when:
- the Unconnected Send response is
0xD2with non-zero general status, and - no route path is configured (direct mode cannot carry a route path).
Sourcepub async fn unregister_session(&mut self) -> Result<()>
pub async fn unregister_session(&mut self) -> Result<()>
Unregisters the EtherNet/IP session with the PLC
Sourcepub fn build_element_id_segment(&self, index: u32) -> Vec<u8> ⓘ
pub fn build_element_id_segment(&self, index: u32) -> Vec<u8> ⓘ
Builds an Element ID segment for array element addressing
Reference: 1756-PM020, Pages 603-611, 870-890 (Element ID Segment Size Selection)
Element ID segments use different sizes based on index value:
- 0-255: 8-bit Element ID (0x28 + 1 byte value)
- 256-65535: 16-bit Element ID (0x29 0x00 + 2 bytes low, high)
- 65536+: 32-bit Element ID (0x2A 0x00 + 4 bytes lowest to highest)
Sourcepub fn build_base_tag_path(&self, tag_name: &str) -> Vec<u8> ⓘ
pub fn build_base_tag_path(&self, tag_name: &str) -> Vec<u8> ⓘ
Builds base tag path without array element addressing
Extracts the base tag name from array notation (e.g., "MyArray[5]" -> "MyArray")
Reference: 1756-PM020, Page 894-909 (ANSI Extended Symbol Segment Construction)
Sourcepub fn build_read_array_request(
&self,
base_array_name: &str,
start_index: u32,
element_count: u16,
) -> Vec<u8> ⓘ
pub fn build_read_array_request( &self, base_array_name: &str, start_index: u32, element_count: u16, ) -> Vec<u8> ⓘ
Builds a CIP Read Tag Service request for array elements with element addressing
This method uses proper CIP element addressing (0x28/0x29/0x2A segments) in the Request Path to read specific array elements or ranges.
Reference: 1756-PM020, Pages 603-611, 815-851 (Array Element Addressing Examples)
§Arguments
base_array_name- Base name of the array (e.g.,"MyArray"for"MyArray[10]")start_index- Starting element index (0-based)element_count- Number of elements to read
§Example
Reading elements 10-14 of array “MyArray” (5 elements):
let request = client.build_read_array_request("MyArray", 10, 5);This generates:
- Request Path:
0x91 "MyArray" 0x28 0x0A(element 10) - Request Data:
0x05 0x00(5 elements)