Gotchas
Each entry starts with the symptom, then gives the root cause and a complete Rust fix. The examples use TCP 192.168.250.100:1025 and SlmpPlcProfile::IqR unless the profile itself is the point.
LTN/LSTN/LCN/LZ reads return wrong values
| Symptom |
Root cause |
Fix |
LTN0, LSTN0, LCN0, or LZ0 looks truncated or is rejected. |
These current-value families are 32-bit values, not normal 16-bit word values. |
Use named addresses with :D or :L, or call read_typed with a 32-bit dtype. |
use plc_comm_slmp::{
read_named, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqR);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
let addresses = vec![
"LTN0:D".to_string(),
"LSTN0:D".to_string(),
"LCN0:L".to_string(),
"LZ0:D".to_string(),
];
let values = read_named(&client, &addresses).await?;
println!("{values:?}");
client.close().await?;
Ok(())
}
LCS/LCC reads look incorrect
| Symptom |
Root cause |
Fix |
LCS0 or LCC0 does not behave like a normal word value. |
Long counter state devices are state bits. Reads use direct bit access, and writes route through random bit write (0x1402). |
Use read_named or write_typed with SlmpValue::Bool. |
use plc_comm_slmp::{
read_named, write_typed, SlmpAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
SlmpValue,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqR);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
let addresses = vec!["LCS0".to_string(), "LCC0".to_string()];
let state = read_named(&client, &addresses).await?;
write_typed(&client, SlmpAddress::parse("LCC0")?, "BIT", &SlmpValue::Bool(true)).await?;
println!("{state:?}");
client.close().await?;
Ok(())
}
LTS/LTC/LSTS/LSTC write rejected
| Symptom |
Root cause |
Fix |
| Direct bit writes to long timer state devices are rejected. |
These families need the helper route that selects supported random bit write behavior. |
Use write_named or write_typed for the state device. |
use plc_comm_slmp::{
write_named, NamedAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile, SlmpValue,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqR);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
let mut updates = NamedAddress::new();
updates.insert("LTS0".to_string(), SlmpValue::Bool(true));
updates.insert("LTC0".to_string(), SlmpValue::Bool(false));
write_named(&client, &updates).await?;
client.close().await?;
Ok(())
}
G/HG fails
| Symptom |
Root cause |
Fix |
G100 or HG1000 fails through the high-level helpers. |
Module buffer access is outside the public typed helper surface. |
Use extended-device methods with qualified addresses such as U3\G100 or U3E0\HG0. |
use plc_comm_slmp::{
parse_qualified_device, SlmpClient, SlmpConnectionOptions, SlmpExtensionSpec, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqR);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
let device = parse_qualified_device("U3\\G100")?;
let values = client.read_words_extended(device, 4, SlmpExtensionSpec::default()).await?;
println!("{values:?}");
client.close().await?;
Ok(())
}
Mixed word and bit write fails
| Symptom |
Root cause |
Fix |
| A block write that combines word blocks and bit blocks returns a PLC-side error. |
Some PLC paths reject command 0x1406 for mixed word and bit payloads. |
Split word and bit writes, or use SlmpBlockWriteOptions { split_mixed_blocks: true } intentionally. |
use plc_comm_slmp::{
SlmpAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile, SlmpValue, write_typed,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqR);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
write_typed(&client, SlmpAddress::parse("D100")?, "U", &SlmpValue::U16(42)).await?;
write_typed(&client, SlmpAddress::parse("M100")?, "BIT", &SlmpValue::Bool(true)).await?;
client.close().await?;
Ok(())
}
DX/DY fails on SlmpPlcProfile::IqF
| Symptom |
Root cause |
Fix |
DX or DY is rejected with SlmpPlcProfile::IqF. |
iQ-F does not support DX and DY in this profile. |
Use X and Y; iQ-F string notation is octal. |
use plc_comm_slmp::{
parse_device_for_plc_profile, read_typed, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqF);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
let x100 = parse_device_for_plc_profile("X100", SlmpPlcProfile::IqF)?;
let value = read_typed(&client, x100, "BIT").await?;
println!("{value:?}");
client.close().await?;
Ok(())
}
All reads return an end code
| Symptom |
Root cause |
Fix |
Simple reads such as D100 connect but return an SLMP end code. |
SlmpPlcProfile selects the frame type, compatibility mode, and address parsing rules. The wrong profile can make every request invalid for your PLC. |
Choose the concrete profile from PROFILES.md in your configuration or UI. |
use plc_comm_slmp::{
read_typed, SlmpAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqR);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
match read_typed(&client, SlmpAddress::parse("D100")?, "U").await {
Ok(value) => println!("{value:?}"),
Err(error) if error.end_code.is_some() => {
println!("PLC rejected the request: {:?}", error.end_code);
}
Err(error) => return Err(error.into()),
}
client.close().await?;
Ok(())
}
Missing or non-canonical profile is rejected
| Symptom |
Root cause |
Fix |
Text configuration such as iq-r or MELSEC:IQ-R cannot be converted to a profile. |
SlmpPlcProfile::parse_label accepts only exact canonical profile values, and SlmpConnectionOptions::new requires a concrete Rust selector. There is no Unspecified fallback in this crate. |
Store only canonical profiles such as melsec:iq-r, or let your UI store the selector directly. |
use plc_comm_slmp::{SlmpConnectionOptions, SlmpPlcProfile};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let profile = SlmpPlcProfile::parse_label("melsec:iq-r").ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::InvalidInput, "unsupported PLC profile")
})?;
let mut options = SlmpConnectionOptions::new("192.168.250.100", profile);
options.port = 1025;
println!("{}", options.plc_profile().canonical_name());
Ok(())
}
Concurrent callers are serialized on one connection
| Symptom |
Root cause |
Fix |
| Two async tasks sharing one PLC connection appear to run one request at a time. |
A single SLMP connection is one ordered frame stream. The public SlmpClient is cloneable and internally serialized to keep responses matched to requests. |
Share SlmpClient clones for safety, or open separate connections only when your PLC and network design allow it. |
use plc_comm_slmp::{
read_typed, SlmpAddress, SlmpClient, SlmpConnectionOptions, SlmpPlcProfile,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut options = SlmpConnectionOptions::new("192.168.250.100", SlmpPlcProfile::IqR);
options.port = 1025;
let client = SlmpClient::connect(options).await?;
let left_address = SlmpAddress::parse("D100")?;
let right_address = SlmpAddress::parse("D101")?;
let (left, right) = tokio::join!(
read_typed(&client, left_address, "U"),
read_typed(&client, right_address, "U"),
);
println!("{:?} {:?}", left?, right?);
client.close().await?;
Ok(())
}
X/Y addresses look shifted on iQ-F
| Symptom |
Root cause |
Fix |
X100 or Y100 points to a different I/O address than expected after a profile change. |
iQ-F uses octal X/Y text; non-iQ-F profiles use hexadecimal text. |
Parse string addresses with the same SlmpPlcProfile you use for the connection. |
use plc_comm_slmp::{parse_device_for_plc_profile, SlmpPlcProfile};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let iqf_x = parse_device_for_plc_profile("X100", SlmpPlcProfile::IqF)?;
let iqr_x = parse_device_for_plc_profile("X20", SlmpPlcProfile::IqR)?;
println!("{iqf_x:?} {iqr_x:?}");
Ok(())
}
S appears in a catalog but parsing fails
| Symptom |
Root cause |
Fix |
The live range catalog reports S, but S cannot be parsed as a public device address. |
The crate intentionally does not expose the MELSEC step-relay device in the parser/client surface. |
Keep S out of application address lists until support is added deliberately. |
use plc_comm_slmp::SlmpAddress;
fn main() {
assert!(SlmpAddress::try_parse("S0").is_none());
}