Usage guide
Recommended entry points
| Entry point | Use it for |
|---|---|
SlmpConnectionOptions::new(host, port, transport, target, plc_profile) |
Creating a connection configuration with an explicit endpoint, route, and profile; only timeout, monitoring timer, and keepalive use approved defaults. |
SlmpClient::connect(options) |
Opening a TCP or UDP SLMP client. |
SlmpAddress::parse("D100", plc_profile) |
Parsing a profile-bound device address. |
read_latest_self_diagnosis_error_code |
Reading the latest PLC self-diagnosis error code from SD0. |
read_typed and write_typed |
Reading or writing one scalar value. |
read_named and write_named |
Reading or writing a small typed collection by address text. |
read_words_single_request |
Reading one contiguous word range in one request. |
read_dwords_single_request |
Reading one contiguous 32-bit range in one request. |
write_bit_in_word |
Updating one bit inside a word register. |
poll_named |
Repeating a named typed collection read on an interval. |
parse_qualified_device |
Parsing extended device text such as U3\G100, U3E0\HG0, and J2\SW10. |
read_words_extended / write_words_extended |
Reading or writing routed U... / J... word devices. |
read_bits_extended / write_bits_extended |
Reading or writing routed U... / J... bit devices. |
Connection
SlmpConnectionOptions exposes user-settable connection fields. The PLC profile, frame type, and compatibility mode are selected together by SlmpConnectionOptions::new.
| Field | Default | Meaning |
|---|---|---|
host |
value passed to new |
PLC host name or IP address. |
port |
required argument | TCP or UDP destination port. |
timeout |
3 seconds | Absolute deadline used separately for complete connection establishment and for each complete request. |
tcp_keepalive |
30 seconds | TCP keepalive idle time, or None. |
target |
required argument | SLMP target address fields; pass SlmpTargetAddress::default() explicitly for the own station. |
transport_mode |
required argument | TCP or UDP. |
monitoring_timer |
0x0010 |
SLMP monitoring timer. |
SlmpClient::connect creates one monotonic connection deadline before IPv4
hostname resolution or socket work. TCP resolution, every selected IPv4
candidate, no-delay and keepalive configuration, and final adoption share that
single deadline. UDP resolution, bind, connect, and adoption use the same rule.
An IPv4 literal bypasses DNS. The library never tries IPv6 or gives each phase
or candidate a fresh timeout.
Connection-deadline expiry returns SlmpErrorKind::Timeout and exposes no
client. If all selected candidates fail before expiry, the error is
SlmpErrorKind::Transport with the connection cause. Dropping the connect
future cancels the public operation; a platform DNS call that cannot be stopped
may finish in its background task, but its result is discarded and cannot
create or mutate a client.
use std::time::Duration;
use plc_comm_slmp::{
SlmpClient, SlmpConnectionOptions, SlmpPlcProfile, SlmpTransportMode,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
options.timeout = Duration::from_secs(3);
let client = SlmpClient::connect(options).await?;
println!("{:?}", client.plc_profile().await);
client.close().await?;
Ok(())
}
Remote password
Remote password lock/unlock commands are available on SlmpClient.
The Rust high-level connection does not automatically unlock or lock a remote password.
If your PLC route uses remote password protection, unlock after connecting and lock before closing.
Always attempt re-lock after a confirmed unlock, even when the intervening
operation fails. If unlock or re-lock has an outcome-unknown result, do not
retry automatically; reopen, inspect the PLC lock state, and reconcile it
explicitly because access may remain unlocked.
let password_address =
SlmpAddress::parse("D100", client.plc_profile().await)?;
client.remote_password_unlock("secret").await?;
let read_result = read_typed(&client, password_address, "U").await;
let lock_result = client.remote_password_lock("secret").await;
lock_result?;
let value = read_result?;
For C200-series password end codes, see the shared
SLMP Troubleshooting & Codes
page.
Remote CPU control
remote_run and remote_pause require SlmpRemoteMode::Normal or
SlmpRemoteMode::Force. remote_run also requires one explicit
SlmpRemoteClearMode; no magic numeric fallback is used. remote_reset() sends
the fixed RESET request and returns after the send completes because a successful
RESET does not return the normal response. The client then closes its transport so
a delayed RESET response cannot be consumed by another 3E request. Create a new
client connection and confirm the PLC state when proof of the reset is required.
Routing / target station
Every connection explicitly selects its complete target route. For a directly
connected own station, pass SlmpTargetAddress::default() deliberately. Use
other values when the PLC network is configured for another station, multi-CPU
module I/O, or multidrop access.
SlmpTargetAddress controls the SLMP destination header. It is not a device
family selector; routed devices such as Un\Gn and Jn\... still need their
own address syntax.
use plc_comm_slmp::{SlmpConnectionOptions, SlmpModuleIo, SlmpPlcProfile, SlmpTargetAddress};
let target = SlmpTargetAddress {
network: 0x01,
station: 0x02,
module_io: SlmpModuleIo::OWN_STATION,
multidrop: 0x00,
};
let options = SlmpConnectionOptions::new(
"192.168.250.100",
1025,
plc_comm_slmp::SlmpTransportMode::Tcp,
target,
SlmpPlcProfile::IqR,
)?;
Do not omit the target. SlmpTargetAddress::default() is only a convenient way
to spell the explicit own-station route at the call site.
Responses must return the same network, station, module I/O, and multidrop route. The client discards complete foreign-route responses while waiting within the original request deadline. A 4E response must also match the request serial.
Extended device access
G, HG, and J devices are not normal standalone addresses. Use the
extended device APIs with a qualified address:
| Address form | Meaning |
|---|---|
U3\G100 |
Module access buffer memory G100 on unit U3. |
U3E0\HG0 |
CPU buffer memory HG0 on U3E0, when the selected profile supports it. |
J2\SW10 |
Link direct SW10 on J network 2. |
J1\X10 |
Link direct X10 on J network 1. |
The selected PLC profile and the actual PLC configuration still decide whether the route is accepted. The write below is a controlled-test example: reserve the exact configured module-buffer range and do not use a production control buffer.
use plc_comm_slmp::{
parse_qualified_device, SlmpClient, SlmpConnectionOptions, SlmpDeviceModification,
SlmpPlcProfile,
};
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let module = parse_qualified_device(r"U3\G100", SlmpPlcProfile::IqR)?;
let module_words = client.read_words_extended(module, 4).await?;
client.write_words_extended(module, &[1, 2, 3, 4]).await?;
let module_readback_result = client.read_words_extended(module, 4).await;
let module_restore_result = client.write_words_extended(module, &module_words).await;
module_restore_result?;
let module_readback = module_readback_result?;
let cpu_buffer = parse_qualified_device(r"U3E0\HG0", SlmpPlcProfile::IqR)?;
let cpu_buffer_words = client.read_words_extended(cpu_buffer, 2).await?;
let link_word = parse_qualified_device(r"J2\SW10", SlmpPlcProfile::IqR)?;
let link_words = client.read_words_extended(link_word, 1).await?;
let link_bits = parse_qualified_device(r"J1\X10", SlmpPlcProfile::IqR)?;
let bits = client.read_bits_extended(link_bits, 16).await?;
let indexed = parse_qualified_device(r"U3\D100", SlmpPlcProfile::IqR)?
.with_modification(SlmpDeviceModification::IndexZ(4))?;
let indexed_words = client.read_words_extended(indexed, 1).await?;
client.close().await?;
Restoration is attempted before a readback error is propagated. If the module
write itself returns an outcome-unknown error, do not automatically restore or
retry; reopen the session, inspect the configured buffer, and choose recovery
explicitly. If restoration fails, inspect the configured buffer and reconcile
it manually before continuing. module_readback, cpu_buffer_words, link_words, bits, and
indexed_words are the values available to the surrounding application.
For iQ-R multi-CPU U3En\HG... access, the qualified device never changes the
SLMP request target automatically. Select the target CPU in
SlmpConnectionOptions when a write must be reflected there; use a separate
client for a different target. A write can return a normal end code without
changing the intended CPU buffer when the selected request target identifies a
different CPU or Own Station. Cross-CPU reads remain valid. See the shared
iQ-R target guidance.
Monitor, self-test, and Clear Error
Registration and every monitor cycle are separate one-request operations.
run_monitor_cycle requires the registered Word and DWord counts every time;
it does not auto-register, retry, or infer them. Calling it before monitor
registration still sends one cycle request, so the PLC determines the returned
error. The combined expected count must be nonzero and cannot exceed the
selected profile's monitor-registration limit.
Monitor registration changes the PLC's active monitor set. Use only a known device list on a controlled PLC. If registration has an outcome-unknown result, do not register again automatically; reconnect and reconcile the monitor state explicitly.
use plc_comm_slmp::parse_device;
let word = parse_device("D120", SlmpPlcProfile::IqR)?;
let dword = parse_device("D200", SlmpPlcProfile::IqR)?;
client.register_monitor_devices(&[word], &[dword]).await?;
let cycle = client.run_monitor_cycle(1, 1).await?;
let echo = client.self_test_loopback(b"A1B2C3D4").await?;
Self-test accepts only 1–960 ASCII 0-9/A-F bytes and requires exact declared
length, actual length, and echo equality.
Clear Error is a separate state-changing maintenance action. It clears the PLC's current error state and cannot recreate the diagnostics that existed before the command. Capture the required diagnostics first and invoke it only on a controlled PLC when clearing the error is explicitly intended:
client.clear_error().await?;
clear_error has no wire-level arguments and always sends the fixed
empty-payload command.
PLC diagnostics
SlmpClient::read_latest_self_diagnosis_error_code reads SD0, the latest PLC self-diagnosis error code, and returns the raw 16-bit value. Format it as hexadecimal when displaying it.
This value is separate from SlmpError.end_code. SlmpError.end_code is the SLMP response end code for a communication request, while SD0 is the PLC CPU's self-diagnosis error register.
use plc_comm_slmp::{
SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let error_code = client.read_latest_self_diagnosis_error_code().await?;
println!("latest self-diagnosis error code: 0x{error_code:04X}");
client.close().await?;
Ok(())
}
SLMP response end codes
When the PLC returns a non-zero SLMP end code, high-level calls return SlmpError.
Read end_code for the PLC response code and error_info when the PLC returned the structured error-information block. The library accepts that PLC error as definitive only when the embedded route, command, and subcommand match the active request. A mismatch is malformed, retires the transport, and makes a transmitted state change outcome-unknown. error_info.raw contains the required nine-byte prefix and error_info.extra retains every following byte.
Each request gets one monotonic deadline immediately before its first transport send. Queue
waiting does not consume that deadline. The same deadline covers the complete send, TCP/UDP
response assembly, response correlation, protocol parsing, and command-specific payload decode.
Request-exchange deadline expiry on a read returns SlmpErrorKind::Timeout; callers may also use
SlmpError::is_timeout() instead of matching error-message text. Timeout invalidates the
in-flight transport, so explicitly connect a new client before another request.
SlmpErrorKind distinguishes validation (General), Timeout, Cancelled, Closed,
NotConnected, Transport, MalformedResponse, PlcEndCode, ProfileFeature, and
OutcomeUnknown. If transmission of a state-changing command may have started, timeout,
cancellation reported by an adapter, close, transport failure, or malformed acknowledgement returns OutcomeUnknown;
inspect outcome_unknown_reason for the structured cause. Do not automatically resend that
command. A nonzero PLC end code remains the definitive PlcEndCode result and does not by itself
retire a fully correlated connection.
Once the complete response has been correlated, protocol-checked, and decoded, that definitive
success or PLC end code wins over a concurrent close or a deadline observed only after decode.
The transport is still retired when either local condition occurred. A close that arrives before
definitive decode still returns Closed for a read or OutcomeUnknown/Closed for a transmitted
state-changing command; an unfinished decode may likewise retain timeout classification.
Dropping a Rust future (including an outer tokio::time::timeout) cannot deliver a library
Result; it retires an in-flight transport. If the dropped command was state-changing, the caller
must treat its outcome as unknown and must not resend it automatically.
match read_typed(
&client,
SlmpAddress::parse("D100", client.plc_profile().await)?,
"U",
).await {
Ok(value) => println!("D100 = {value:?}"),
Err(error) => {
if let Some(end_code) = error.end_code {
println!("SLMP end_code=0x{end_code:04X}");
}
if let Some(info) = error.error_info.as_ref() {
println!("command=0x{:04X}", info.command);
println!("subcommand=0x{:04X}", info.subcommand);
println!("additional error bytes={:02X?}", info.extra);
}
}
}
Read a single value
use plc_comm_slmp::{
read_typed, SlmpAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let value = read_typed(&client, SlmpAddress::parse("D100", SlmpPlcProfile::IqR)?, "U").await?;
println!("{:?}", value);
client.close().await?;
Ok(())
}
| Suffix | Rust value | Meaning |
|---|---|---|
U |
SlmpValue::U16 |
Unsigned 16-bit word. |
S |
SlmpValue::I16 |
Signed 16-bit word. |
D |
SlmpValue::U32 |
Unsigned 32-bit value. |
L |
SlmpValue::I32 |
Signed 32-bit value. |
F |
SlmpValue::F32 |
32-bit float. |
BIT |
SlmpValue::Bool |
Bit device value. |
write_typed requires the exact matching SlmpValue variant shown above. It
does not coerce Boolean, floating-point, signed, unsigned, or differently sized
values. F32 values must also be finite. Text accepted by CLI helpers is range
checked before it is converted into a SlmpValue.
Write a single value
use plc_comm_slmp::{
read_typed, write_typed, SlmpAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
SlmpValue,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let device = SlmpAddress::parse("D600", SlmpPlcProfile::IqR)?;
let original = read_typed(&client, device, "U").await?;
write_typed(&client, device, "U", &SlmpValue::U16(42)).await?;
let readback_result = read_typed(&client, device, "U").await;
let restore_result = write_typed(&client, device, "U", &original).await;
restore_result?;
let value = readback_result?;
println!("{:?}", value);
client.close().await?;
Ok(())
}
Use only a register reserved for controlled testing. The example attempts restoration before propagating a readback error. An outcome-unknown test write requires explicit state inspection rather than automatic restore or retry. If restoration fails, inspect the register and reconcile its value manually before continuing.
Named typed collection
use plc_comm_slmp::{
read_named, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let addresses = vec![
"D100:U".to_string(),
"D200:F".to_string(),
"D50.3".to_string(),
"M100:BIT".to_string(),
];
let values = read_named(&client, &addresses).await?;
println!("{:?}", values);
client.close().await?;
Ok(())
}
read_named sends exactly one random-read request. An oversized batch or an
entry that requires a direct, block, or long-timer helper route is rejected
before transport. Use the explicit typed or long-device API when another
command family is required.
write_named sends exactly one random-write request. Word and DWord entries may
share that request; bit entries form a separate request family. Mixing bit and
word/DWord entries is rejected before transport, so the application must perform
and account for the separate operations explicitly. Bit-in-word entries are also
rejected here because they require a read-modify-write sequence; call
write_bit_in_word explicitly when that non-atomic sequence is intended.
write_bit_in_word performs one read followed by one write while holding one FIFO client turn, so
another operation on the same client cannot interleave between those two frames. It is still a
non-atomic PLC read-modify-write: another PLC client or PLC logic can change the word, and a
post-send interruption of its write reports OutcomeUnknown. FIFO wait is outside the timeout;
one absolute deadline covers both requests after admission, and a successful read always proceeds
to the write even when the selected bit is unchanged. Use write_bit_in_word_extended with a
SlmpQualifiedDeviceAddress for an immutable U-qualified module-buffer or J-qualified link-direct
route. Unsupported profile/route combinations fail before the read. Do not retry automatically.
Shared-client ordering and close
Clones of one SlmpClient share one FIFO admission queue and allow at most one wire transaction at
a time. Arguments are fixed for the queued call; cancelling a waiting Rust future removes that
waiter without sending and without delaying later waiters. Calling close invalidates the exact
client connection generation immediately. An active incomplete read returns Closed, an active
incomplete transmitted state-changing command returns OutcomeUnknown with reason Closed, and
queued calls return Closed without sending. A fully correlated and decoded response remains the
definitive result even if close wins the later transport-state race. Separate SlmpClient
instances have independent queues and can progress concurrently.
Single-request range reads
The library rejects ranges above the protocol/profile limit. It does not split one logical read into multiple requests because the first and last values would then be sampled at different times. If multiple requests are intentional, build that sequence in the application and handle the temporal split explicitly.
Every contiguous request must also fit the address field selected by the wire
format: Q/L-compatible and link-direct layouts use 24 bits, while iQ-R layouts
use 32 bits. A J-qualified link-direct request always uses the 24-bit Q/L layout,
even when the client profile is iQ-R; other iQ-R Extended Device layouts use
32 bits. Admission uses the complete consumed span, not the configured PLC
device-range catalog. Word-unit access to a word device consumes one device per
word; word-unit packed access to a bit device consumes 16 bit devices per word;
ordinary DWord/Float32 access consumes two word devices per value (32 bit-device
numbers per value when packed through a bit family); and one bit-block point
consumes 16 bit devices. The long-timer Direct status block is the explicit
exception: four returned words consume one LTN/LSTN device. Random and
monitor DWord entries use the same route-specific logical widths. A span that
crosses the wire maximum is rejected before request counters or transport; the
library does not substitute profile usable-range policy for this
wire-representability check.
use plc_comm_slmp::{
read_words_single_request, SlmpAddress, SlmpClient, SlmpConnectionOptions,
SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let start = SlmpAddress::parse("D100", SlmpPlcProfile::IqR)?;
let single = read_words_single_request(&client, start, 8).await?;
println!("{:?}", single);
client.close().await?;
Ok(())
}
Bit in word
Use .n notation when reading through read_named, and use write_bit_in_word when you need to update one bit inside a word.
The helper requires a word device and preflights writability before sending its
read request, so an invalid or non-writable target sends neither half of the
read-modify-write sequence.
use plc_comm_slmp::{
read_named, read_typed, write_bit_in_word, write_bit_in_word_extended, write_typed, SlmpAddress, SlmpClient,
SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let word = SlmpAddress::parse("D50", SlmpPlcProfile::IqR)?;
let original = read_typed(&client, word, "U").await?;
write_bit_in_word(&client, word, 3, true).await?;
// A qualified route uses the explicit sibling helper:
// let qualified = plc_comm_slmp::parse_qualified_device(r"J1\W10", SlmpPlcProfile::IqR)?;
// write_bit_in_word_extended(&client, qualified, 3, true).await?;
let addresses = vec!["D50.3".to_string()];
let snapshot_result = read_named(&client, &addresses).await;
let restore_result = write_typed(&client, word, "U", &original).await;
restore_result?;
let snapshot = snapshot_result?;
println!("{:?}", snapshot);
client.close().await?;
Ok(())
}
Use only a word reserved for controlled testing. Restoration is attempted before a snapshot-read error is propagated. If the bit write has an unknown outcome, inspect and reconcile the whole word instead of retrying automatically. If restoration fails, inspect the whole word and reconcile it manually before continuing.
Polling
use std::time::Duration;
use futures_util::StreamExt;
use plc_comm_slmp::{
poll_named, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let addresses = vec!["D100:U".to_string(), "M100:BIT".to_string(), "D50.3".to_string()];
let mut stream = Box::pin(poll_named(&client, &addresses, Duration::from_millis(500)));
if let Some(snapshot) = stream.next().await.transpose()? {
println!("{:?}", snapshot);
}
client.close().await?;
Ok(())
}
The address plan, compact result indexes, and immutable Random Read payload are validated and prepared once when the polling stream is created. Each cycle reuses that prepared request while retaining the ordinary client FIFO, deadline, cancellation, close, and error contracts.
Operational recipes
The multi_plc_monitor example is a read-only multi-PLC monitor. It polls each
PLC in an independent reconnect loop, logs connected, lost, reconnecting,
and recovered transitions, and uses the same 1 second to 30 second backoff as
the reconnect polling sample.
The config_polling example is read-only JSON-driven polling. Its CSV output
uses the shared long form timestamp,plc,tag,value. YAML config is only
available in the Python recipes; the Rust recipe intentionally accepts JSON.
Runnable examples
The repository examples are designed to run from environment variables.
| Variable | Requirement | Meaning |
|---|---|---|
SLMP_HOST |
optional; example host only | PLC or mock server host. |
SLMP_PORT |
required | TCP/UDP destination port; zero is rejected. |
SLMP_PLC_PROFILE |
required | Canonical profile such as melsec:iq-r or melsec:iq-f. |
SLMP_TRANSPORT |
required | Exactly tcp or udp. |
SLMP_TARGET |
required unless all four route fields are present | SELF, SELF-MULTIPLE-CPU-1, or NAME,NET,ST,IO,MD. |
SLMP_NETWORK / SLMP_STATION |
complete-set alternative | Both are required together with module I/O and multidrop. |
SLMP_MODULE_IO / SLMP_MULTIDROP |
complete-set alternative | Both are required together with network and station. |
SLMP_TIMEOUT_MS |
optional; 3000 |
Absolute request-exchange deadline in milliseconds. |
SLMP_MONITORING_TIMER |
optional; 16 |
SLMP monitoring timer in 250 ms units (4 seconds). |
SLMP_ENABLE_WRITES |
optional; 0 |
Set 1 to enable write examples. |
Raw read/write:
cd plc-comm-slmp-rust
SLMP_HOST=192.168.250.100 \
SLMP_PORT=1025 \
SLMP_TRANSPORT=tcp \
SLMP_TARGET=SELF \
SLMP_PLC_PROFILE=melsec:iq-r \
cargo run --example raw_read_write
Named helpers:
cd plc-comm-slmp-rust
SLMP_HOST=192.168.250.100 \
SLMP_PORT=1025 \
SLMP_TRANSPORT=tcp \
SLMP_TARGET=SELF \
SLMP_PLC_PROFILE=melsec:iq-f \
SLMP_NAMED_ADDRESSES='D100:U,D200:F,D50.3,M100:BIT' \
cargo run --example named_helpers
Advanced operations:
cd plc-comm-slmp-rust
SLMP_HOST=192.168.250.100 \
SLMP_PORT=1025 \
SLMP_TRANSPORT=tcp \
SLMP_TARGET=SELF \
SLMP_PLC_PROFILE=melsec:iq-r \
SLMP_RANDOM_WORDS='D100,R10' \
SLMP_RANDOM_DWORDS='D200,LTN10' \
SLMP_EXT_DEVICE='J1\W10' \
cargo run --example advanced_operations
Device matrix compare:
cd plc-comm-slmp-rust
SLMP_HOST=192.168.250.100 \
SLMP_PORT=1025 \
SLMP_TRANSPORT=tcp \
SLMP_TARGET=SELF \
SLMP_PLC_PROFILE=melsec:iq-r \
cargo run --example device_matrix_compare
Device range catalog
Every semantic SlmpDeviceAddress or qualified address is bound to the exact canonical profile used to create it. Passing it to a client configured for any other profile is rejected before request construction or transport activity, including when a unit-specific profile shares a base family with the client. Parse the address again with the destination client's profile instead of reusing it across profiles.
read_device_range_catalog reads the canonical profile's required SD-register window after you connect. It requires an explicit profile through SlmpConnectionOptions; it does not auto-discover your intended profile, probe candidate addresses, or infer a smaller range from a failed PLC request. Any timeout, transport, protocol, route, password, busy, or other PLC error is returned to the caller; a range without an authoritative value remains unknown. Rust future cancellation drops the operation instead of producing a library error.
The source rules for this catalog are maintained in the shared SLMP device ranges reference.
use plc_comm_slmp::{SlmpClient, SlmpConnectionOptions, SlmpPlcProfile};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let catalog = client.read_device_range_catalog().await?;
if let Some(entry) = catalog.entries.iter().find(|entry| entry.supported) {
println!("{:?}", entry);
}
client.close().await?;
Ok(())
}
Long device families
LTN, LSTN, LCN, and LZ are 32-bit families. Use read_typed with D or L for one scalar
value. LTN and LSTN use the Direct long-timer route and therefore cannot be included in
read_named or poll_named, whose contract is exactly one Random Read request.
use plc_comm_slmp::{
read_typed, SlmpAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let options = SlmpConnectionOptions::new("192.168.250.100", 1025, plc_comm_slmp::SlmpTransportMode::Tcp, plc_comm_slmp::SlmpTargetAddress::default(), SlmpPlcProfile::IqR)?;
let client = SlmpClient::connect(options).await?;
let timer = read_typed(
&client,
SlmpAddress::parse("LTN10", SlmpPlcProfile::IqR)?,
"D",
).await?;
let counter = read_typed(
&client,
SlmpAddress::parse("LCN30", SlmpPlcProfile::IqR)?,
"D",
).await?;
println!("timer={timer:?}, counter={counter:?}");
client.close().await?;
Ok(())
}
Caution: Plain word access to LTN/LSTN/LCN/LZ is rejected by guarded semantic routes. Use
read_typed/write_typedwithDorL, or an explicit long-device helper. Low-level explicit packed-word APIs remain separate wire-level operations.
Address reference table
| Form | Example | Meaning |
|---|---|---|
:U |
D100:U |
Unsigned 16-bit word. |
:S |
D100:S |
Signed 16-bit word. |
:D |
D100:D |
Unsigned 32-bit value. |
:L |
D100:L |
Signed 32-bit value. |
:F |
D100:F |
32-bit float. |
:BIT |
M100:BIT |
Boolean bit device value. |
.n |
D50.3 |
Bit n inside a word, where n is 0 through F. |
Named addresses used with read_named, write_named, and poll_named must include the intended
type, for example D100:U or M100:BIT. BIT is valid only for bit devices; numeric/string dtypes
are valid only for word devices. A word-device bit is written explicitly with .n or
write_bit_in_word; no helper performs an implicit mask, read-modify-write, or fallback.
Request payload limits
One SLMP request can carry at most 65,529 command-payload bytes over TCP. UDP must also fit one complete datagram, so the command-payload maximum is 65,492 bytes for 3E and 65,488 bytes for 4E. Array and random label requests use even-sized payloads and therefore have a largest protocol-representable payload of 65,528 bytes before the lower UDP limit is applied.
Oversized requests return SlmpError before send, traffic counters, request-frame publication, or
4E serial allocation. They are never truncated or split automatically; applications that issue
several requests must define ordering, partial-success, and write-atomicity behavior.
Traffic statistics
client.traffic_stats().await returns the client-lifetime request_count, tx_bytes, and
rx_bytes snapshot. A request counts only after its complete frame is sent; a complete received
frame counts before protocol or PLC end-code validation. Close and reconnect do not reset it.