use crate::address::device_spec_size;
use crate::device_ranges::{
SlmpDeviceRangeCatalog, SlmpDeviceRangeFamily,
build_catalog_for_family as build_device_range_catalog_for_family,
read_registers as read_device_range_registers,
resolve_profile_for_family as resolve_device_range_profile_for_family,
};
use crate::error::SlmpError;
use crate::model::{
SlmpBlockRead, SlmpBlockReadResult, SlmpBlockWrite, SlmpBlockWriteOptions, SlmpCommand,
SlmpCompatibilityMode, SlmpConnectionOptions, SlmpCpuOperationState, SlmpCpuOperationStatus,
SlmpDeviceAddress, SlmpDeviceCode, SlmpExtensionSpec, SlmpFrameType, SlmpLabelArrayReadPoint,
SlmpLabelArrayReadResult, SlmpLabelArrayWritePoint, SlmpLabelRandomReadResult,
SlmpLabelRandomWritePoint, SlmpLongTimerResult, SlmpPlcFamily, SlmpQualifiedDeviceAddress,
SlmpRandomReadResult, SlmpTargetAddress, SlmpTrafficStats, SlmpTransportMode, SlmpTypeNameInfo,
};
use std::net::{TcpStream as StdTcpStream, ToSocketAddrs};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpStream, UdpSocket};
use tokio::sync::Mutex;
use tokio::task;
use tokio::time::timeout;
const MAX_RUNTIME_RANGE_PROBE_COUNT: u32 = 1_048_576;
const UDP_RECEIVE_BUFFER_SIZE: usize = 65_535;
#[derive(Clone)]
pub struct SlmpClient {
inner: Arc<Mutex<ClientInner>>,
}
enum Transport {
Tcp(TcpStream),
Udp(UdpSocket),
}
struct ClientInner {
options: SlmpConnectionOptions,
transport: Transport,
serial: u16,
last_request_frame: Vec<u8>,
last_response_frame: Vec<u8>,
traffic_stats: SlmpTrafficStats,
}
impl SlmpClient {
pub async fn connect(options: SlmpConnectionOptions) -> Result<Self, SlmpError> {
let transport = match options.transport_mode {
SlmpTransportMode::Tcp => {
let stream = connect_tcp_stream(&options).await?;
Transport::Tcp(stream)
}
SlmpTransportMode::Udp => {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket
.connect((options.host.as_str(), options.port))
.await?;
Transport::Udp(socket)
}
};
Ok(Self {
inner: Arc::new(Mutex::new(ClientInner {
options,
transport,
serial: 0,
last_request_frame: Vec::new(),
last_response_frame: Vec::new(),
traffic_stats: SlmpTrafficStats::default(),
})),
})
}
pub async fn close(&self) -> Result<(), SlmpError> {
let mut inner = self.inner.lock().await;
if let Transport::Tcp(stream) = &mut inner.transport {
stream.shutdown().await?;
}
inner.last_request_frame.clear();
inner.last_response_frame.clear();
Ok(())
}
pub async fn last_request_frame(&self) -> Vec<u8> {
self.inner.lock().await.last_request_frame.clone()
}
pub async fn last_response_frame(&self) -> Vec<u8> {
self.inner.lock().await.last_response_frame.clone()
}
pub async fn traffic_stats(&self) -> SlmpTrafficStats {
self.inner.lock().await.traffic_stats
}
pub async fn plc_family(&self) -> SlmpPlcFamily {
self.inner.lock().await.options.plc_family
}
pub async fn read_type_name(&self) -> Result<SlmpTypeNameInfo, SlmpError> {
self.inner.lock().await.read_type_name().await
}
pub async fn read_cpu_operation_state(&self) -> Result<SlmpCpuOperationState, SlmpError> {
self.inner.lock().await.read_cpu_operation_state().await
}
pub async fn read_device_range_catalog(&self) -> Result<SlmpDeviceRangeCatalog, SlmpError> {
let family = self.configured_device_range_family().await;
let profile = resolve_device_range_profile_for_family(family);
let registers = read_device_range_registers(self, &profile).await?;
let catalog = build_device_range_catalog_for_family(family, ®isters)?;
self.resolve_device_range_runtime_limits(catalog).await
}
pub async fn read_device_range_catalog_for_family(
&self,
family: SlmpDeviceRangeFamily,
) -> Result<SlmpDeviceRangeCatalog, SlmpError> {
let profile = resolve_device_range_profile_for_family(family);
let registers = read_device_range_registers(self, &profile).await?;
let catalog = build_device_range_catalog_for_family(family, ®isters)?;
self.resolve_device_range_runtime_limits(catalog).await
}
async fn resolve_device_range_runtime_limits(
&self,
mut catalog: SlmpDeviceRangeCatalog,
) -> Result<SlmpDeviceRangeCatalog, SlmpError> {
if !matches!(
catalog.family,
SlmpDeviceRangeFamily::QCpu
| SlmpDeviceRangeFamily::LCpu
| SlmpDeviceRangeFamily::QnU
| SlmpDeviceRangeFamily::QnUDV
) {
return Ok(catalog);
}
if catalog.family == SlmpDeviceRangeFamily::QCpu {
let z_count = if self.can_read_one_word(SlmpDeviceCode::Z, 15).await {
16
} else {
10
};
catalog = crate::device_ranges::replace_fixed_point_count(
catalog,
"Z",
z_count,
"Runtime access check",
"QCPU Z register count is selected by probing Z15.",
);
}
let zr_count = self.resolve_readable_point_count(SlmpDeviceCode::ZR).await;
catalog = crate::device_ranges::replace_fixed_point_count(
catalog,
"ZR",
zr_count,
"Runtime access check",
"ZR register count is selected by probing readable ZR addresses.",
);
Ok(crate::device_ranges::replace_fixed_point_count(
catalog,
"R",
zr_count.min(32_768),
"Runtime access check",
"R register count matches the probed ZR size and is capped at R32767.",
))
}
async fn resolve_readable_point_count(&self, device: SlmpDeviceCode) -> u32 {
if !self.can_read_one_word(device, 0).await {
return 0;
}
let upper_limit = MAX_RUNTIME_RANGE_PROBE_COUNT - 1;
let mut low = 0;
let mut high = 1;
while high < upper_limit && self.can_read_one_word(device, high).await {
low = high;
high = ((high * 2) + 1).min(upper_limit);
}
if high == upper_limit && self.can_read_one_word(device, high).await {
return MAX_RUNTIME_RANGE_PROBE_COUNT;
}
let mut left = low + 1;
let mut right = high - 1;
while left <= right {
let mid = left + ((right - left) / 2);
if self.can_read_one_word(device, mid).await {
low = mid;
left = mid + 1;
} else {
if mid == 0 {
break;
}
right = mid - 1;
}
}
low + 1
}
async fn can_read_one_word(&self, device: SlmpDeviceCode, number: u32) -> bool {
self.read_words_raw(SlmpDeviceAddress::new(device, number), 1)
.await
.is_ok()
}
pub async fn configured_device_range_family(&self) -> SlmpDeviceRangeFamily {
map_plc_family_to_range_family(self.inner.lock().await.options.plc_family)
}
pub async fn read_words_raw(
&self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<u16>, SlmpError> {
self.inner.lock().await.read_words_raw(device, points).await
}
pub async fn write_words(
&self,
device: SlmpDeviceAddress,
values: &[u16],
) -> Result<(), SlmpError> {
self.inner.lock().await.write_words(device, values).await
}
pub async fn read_bits(
&self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<bool>, SlmpError> {
self.inner.lock().await.read_bits(device, points).await
}
pub async fn write_bits(
&self,
device: SlmpDeviceAddress,
values: &[bool],
) -> Result<(), SlmpError> {
self.inner.lock().await.write_bits(device, values).await
}
pub async fn read_dwords_raw(
&self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<u32>, SlmpError> {
self.inner
.lock()
.await
.read_dwords_raw(device, points)
.await
}
pub async fn write_dwords(
&self,
device: SlmpDeviceAddress,
values: &[u32],
) -> Result<(), SlmpError> {
self.inner.lock().await.write_dwords(device, values).await
}
pub async fn read_float32s(
&self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<f32>, SlmpError> {
self.inner.lock().await.read_float32s(device, points).await
}
pub async fn write_float32s(
&self,
device: SlmpDeviceAddress,
values: &[f32],
) -> Result<(), SlmpError> {
self.inner.lock().await.write_float32s(device, values).await
}
pub async fn read_words_extended(
&self,
device: SlmpQualifiedDeviceAddress,
points: u16,
extension: SlmpExtensionSpec,
) -> Result<Vec<u16>, SlmpError> {
self.inner
.lock()
.await
.read_words_extended(device, points, extension)
.await
}
pub async fn write_words_extended(
&self,
device: SlmpQualifiedDeviceAddress,
values: &[u16],
extension: SlmpExtensionSpec,
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.write_words_extended(device, values, extension)
.await
}
pub async fn read_bits_extended(
&self,
device: SlmpQualifiedDeviceAddress,
points: u16,
extension: SlmpExtensionSpec,
) -> Result<Vec<bool>, SlmpError> {
self.inner
.lock()
.await
.read_bits_extended(device, points, extension)
.await
}
pub async fn write_bits_extended(
&self,
device: SlmpQualifiedDeviceAddress,
values: &[bool],
extension: SlmpExtensionSpec,
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.write_bits_extended(device, values, extension)
.await
}
pub async fn read_random(
&self,
word_devices: &[SlmpDeviceAddress],
dword_devices: &[SlmpDeviceAddress],
) -> Result<SlmpRandomReadResult, SlmpError> {
self.inner
.lock()
.await
.read_random(word_devices, dword_devices)
.await
}
pub async fn write_random_words(
&self,
word_entries: &[(SlmpDeviceAddress, u16)],
dword_entries: &[(SlmpDeviceAddress, u32)],
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.write_random_words(word_entries, dword_entries)
.await
}
pub async fn write_random_bits(
&self,
bit_entries: &[(SlmpDeviceAddress, bool)],
) -> Result<(), SlmpError> {
self.inner.lock().await.write_random_bits(bit_entries).await
}
pub async fn read_block(
&self,
word_blocks: &[SlmpBlockRead],
bit_blocks: &[SlmpBlockRead],
) -> Result<SlmpBlockReadResult, SlmpError> {
self.inner
.lock()
.await
.read_block(word_blocks, bit_blocks)
.await
}
pub async fn write_block(
&self,
word_blocks: &[SlmpBlockWrite],
bit_blocks: &[SlmpBlockWrite],
options: Option<SlmpBlockWriteOptions>,
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.write_block(word_blocks, bit_blocks, options.unwrap_or_default())
.await
}
pub async fn remote_run(&self, force: bool, clear_mode: u16) -> Result<(), SlmpError> {
self.inner.lock().await.remote_run(force, clear_mode).await
}
pub async fn remote_stop(&self) -> Result<(), SlmpError> {
self.inner.lock().await.remote_stop().await
}
pub async fn remote_pause(&self, force: bool) -> Result<(), SlmpError> {
self.inner.lock().await.remote_pause(force).await
}
pub async fn remote_latch_clear(&self) -> Result<(), SlmpError> {
self.inner.lock().await.remote_latch_clear().await
}
pub async fn remote_reset(&self, expect_response: bool) -> Result<(), SlmpError> {
self.inner.lock().await.remote_reset(expect_response).await
}
pub async fn remote_password_unlock(&self, password: &str) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.remote_password_unlock(password)
.await
}
pub async fn remote_password_lock(&self, password: &str) -> Result<(), SlmpError> {
self.inner.lock().await.remote_password_lock(password).await
}
pub async fn self_test_loopback(&self, data: &[u8]) -> Result<Vec<u8>, SlmpError> {
self.inner.lock().await.self_test_loopback(data).await
}
pub async fn memory_read_words(
&self,
head_address: u32,
word_length: u16,
) -> Result<Vec<u16>, SlmpError> {
self.inner
.lock()
.await
.memory_read_words(head_address, word_length)
.await
}
pub async fn memory_write_words(
&self,
head_address: u32,
values: &[u16],
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.memory_write_words(head_address, values)
.await
}
pub async fn extend_unit_read_words(
&self,
head_address: u32,
word_length: u16,
module_no: u16,
) -> Result<Vec<u16>, SlmpError> {
self.inner
.lock()
.await
.extend_unit_read_words(head_address, word_length, module_no)
.await
}
pub async fn extend_unit_write_words(
&self,
head_address: u32,
module_no: u16,
values: &[u16],
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.extend_unit_write_words(head_address, module_no, values)
.await
}
pub async fn read_array_labels(
&self,
points: &[SlmpLabelArrayReadPoint],
abbreviation_labels: &[String],
) -> Result<Vec<SlmpLabelArrayReadResult>, SlmpError> {
self.inner
.lock()
.await
.read_array_labels(points, abbreviation_labels)
.await
}
pub async fn write_array_labels(
&self,
points: &[SlmpLabelArrayWritePoint],
abbreviation_labels: &[String],
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.write_array_labels(points, abbreviation_labels)
.await
}
pub async fn read_random_labels(
&self,
labels: &[String],
abbreviation_labels: &[String],
) -> Result<Vec<SlmpLabelRandomReadResult>, SlmpError> {
self.inner
.lock()
.await
.read_random_labels(labels, abbreviation_labels)
.await
}
pub async fn write_random_labels(
&self,
points: &[SlmpLabelRandomWritePoint],
abbreviation_labels: &[String],
) -> Result<(), SlmpError> {
self.inner
.lock()
.await
.write_random_labels(points, abbreviation_labels)
.await
}
pub async fn read_long_timer(
&self,
head_no: u32,
points: usize,
) -> Result<Vec<SlmpLongTimerResult>, SlmpError> {
self.inner
.lock()
.await
.read_long_timer(head_no, points)
.await
}
pub async fn read_long_retentive_timer(
&self,
head_no: u32,
points: usize,
) -> Result<Vec<SlmpLongTimerResult>, SlmpError> {
self.inner
.lock()
.await
.read_long_retentive_timer(head_no, points)
.await
}
pub async fn request(
&self,
command: SlmpCommand,
subcommand: u16,
payload: &[u8],
expect_response: bool,
) -> Result<Vec<u8>, SlmpError> {
self.inner
.lock()
.await
.request(command, subcommand, payload, expect_response)
.await
}
}
async fn connect_tcp_stream(options: &SlmpConnectionOptions) -> Result<TcpStream, SlmpError> {
let host = options.host.clone();
let port = options.port;
let timeout_duration = options.timeout;
let std_stream = task::spawn_blocking(move || {
let addrs: Vec<_> = (host.as_str(), port).to_socket_addrs()?.collect();
if addrs.is_empty() {
return Err(SlmpError::new(format!(
"tcp connect failed: no socket addresses resolved for {host}:{port}"
)));
}
let mut last_error = None;
for addr in addrs {
match StdTcpStream::connect_timeout(&addr, timeout_duration) {
Ok(stream) => {
stream.set_nonblocking(true)?;
return Ok(stream);
}
Err(error) => last_error = Some(error),
}
}
Err(last_error
.map(SlmpError::from)
.unwrap_or_else(|| SlmpError::new("tcp connect failed")))
})
.await
.map_err(|error| SlmpError::new(format!("tcp connect task failed: {error}")))??;
TcpStream::from_std(std_stream).map_err(SlmpError::from)
}
fn map_plc_family_to_range_family(family: SlmpPlcFamily) -> SlmpDeviceRangeFamily {
match family {
SlmpPlcFamily::IqF => SlmpDeviceRangeFamily::IqF,
SlmpPlcFamily::IqR => SlmpDeviceRangeFamily::IqR,
SlmpPlcFamily::IqL => SlmpDeviceRangeFamily::IqL,
SlmpPlcFamily::MxF => SlmpDeviceRangeFamily::MxF,
SlmpPlcFamily::MxR => SlmpDeviceRangeFamily::MxR,
SlmpPlcFamily::QCpu => SlmpDeviceRangeFamily::QCpu,
SlmpPlcFamily::LCpu => SlmpDeviceRangeFamily::LCpu,
SlmpPlcFamily::QnU => SlmpDeviceRangeFamily::QnU,
SlmpPlcFamily::QnUDV => SlmpDeviceRangeFamily::QnUDV,
}
}
impl ClientInner {
async fn read_type_name(&mut self) -> Result<SlmpTypeNameInfo, SlmpError> {
let payload = self
.request(SlmpCommand::ReadTypeName, 0x0000, &[], true)
.await?;
if payload.len() < 16 {
return Err(SlmpError::new("read_type_name response too short"));
}
let model = String::from_utf8_lossy(&payload[..16])
.trim_end_matches('\0')
.trim_end()
.to_string();
let (model_code, has_model_code) = if payload.len() >= 18 {
(u16::from_le_bytes([payload[16], payload[17]]), true)
} else {
(0, false)
};
Ok(SlmpTypeNameInfo {
model,
model_code,
has_model_code,
})
}
async fn read_cpu_operation_state(&mut self) -> Result<SlmpCpuOperationState, SlmpError> {
let status_word = self
.read_words_raw(SlmpDeviceAddress::new(SlmpDeviceCode::SD, 203), 1)
.await?
.into_iter()
.next()
.ok_or_else(|| SlmpError::new("read_cpu_operation_state expected one word"))?;
Ok(decode_cpu_operation_state(status_word))
}
async fn read_words_raw(
&mut self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<u16>, SlmpError> {
Self::validate_direct_word_read(device, points)?;
let payload = self.build_read_write_payload(device, points, None, false);
let sub = self.word_subcommand(false);
let data = self
.request(SlmpCommand::DeviceRead, sub, &payload, true)
.await?;
if data.len() != points as usize * 2 {
return Err(SlmpError::new("read_words payload size mismatch"));
}
Ok(data
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect())
}
async fn write_words(
&mut self,
device: SlmpDeviceAddress,
values: &[u16],
) -> Result<(), SlmpError> {
Self::validate_direct_word_write(device)?;
let payload =
self.build_read_write_payload(device, values.len() as u16, Some(values), false);
let sub = self.word_subcommand(false);
let _ = self
.request(SlmpCommand::DeviceWrite, sub, &payload, true)
.await?;
Ok(())
}
async fn read_bits(
&mut self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<bool>, SlmpError> {
Self::validate_direct_bit_read(device)?;
let payload = self.build_read_write_payload(device, points, None, true);
let data = self
.request(
SlmpCommand::DeviceRead,
self.word_subcommand(true),
&payload,
true,
)
.await?;
Self::unpack_bit_values(&data, points as usize)
}
async fn write_bits(
&mut self,
device: SlmpDeviceAddress,
values: &[bool],
) -> Result<(), SlmpError> {
Self::validate_direct_bit_write(device)?;
let words: Vec<u16> = values.iter().map(|value| u16::from(*value)).collect();
let payload =
self.build_read_write_payload(device, values.len() as u16, Some(&words), true);
let _ = self
.request(
SlmpCommand::DeviceWrite,
self.word_subcommand(true),
&payload,
true,
)
.await?;
Ok(())
}
async fn read_dwords_raw(
&mut self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<u32>, SlmpError> {
Self::validate_direct_dword_read(device)?;
let words = self.read_words_raw(device, points * 2).await?;
Ok(words
.chunks_exact(2)
.map(|chunk| chunk[0] as u32 | ((chunk[1] as u32) << 16))
.collect())
}
async fn write_dwords(
&mut self,
device: SlmpDeviceAddress,
values: &[u32],
) -> Result<(), SlmpError> {
Self::validate_direct_dword_write(device)?;
let mut words = Vec::with_capacity(values.len() * 2);
for value in values {
words.push((value & 0xFFFF) as u16);
words.push((value >> 16) as u16);
}
self.write_words(device, &words).await
}
async fn read_float32s(
&mut self,
device: SlmpDeviceAddress,
points: u16,
) -> Result<Vec<f32>, SlmpError> {
Ok(self
.read_dwords_raw(device, points)
.await?
.into_iter()
.map(f32::from_bits)
.collect())
}
async fn write_float32s(
&mut self,
device: SlmpDeviceAddress,
values: &[f32],
) -> Result<(), SlmpError> {
let values: Vec<u32> = values.iter().map(|value| value.to_bits()).collect();
self.write_dwords(device, &values).await
}
async fn read_words_extended(
&mut self,
device: SlmpQualifiedDeviceAddress,
points: u16,
extension: SlmpExtensionSpec,
) -> Result<Vec<u16>, SlmpError> {
Self::validate_direct_word_read(device.device, points)?;
let extension = Self::resolve_effective_extension(device, extension);
let payload =
self.build_read_write_payload_extended(device.device, points, None, extension, false);
let sub = if extension.direct_memory_specification == 0xF9 {
0x0080
} else if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0080
} else {
0x0082
};
let data = self
.request(SlmpCommand::DeviceRead, sub, &payload, true)
.await?;
if data.len() != points as usize * 2 {
return Err(SlmpError::new("read_words_ext payload size mismatch"));
}
Ok(data
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect())
}
async fn write_words_extended(
&mut self,
device: SlmpQualifiedDeviceAddress,
values: &[u16],
extension: SlmpExtensionSpec,
) -> Result<(), SlmpError> {
Self::validate_direct_word_write(device.device)?;
let extension = Self::resolve_effective_extension(device, extension);
let payload = self.build_read_write_payload_extended(
device.device,
values.len() as u16,
Some(values),
extension,
false,
);
let sub = if extension.direct_memory_specification == 0xF9 {
0x0080
} else if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0080
} else {
0x0082
};
let _ = self
.request(SlmpCommand::DeviceWrite, sub, &payload, true)
.await?;
Ok(())
}
async fn read_bits_extended(
&mut self,
device: SlmpQualifiedDeviceAddress,
points: u16,
extension: SlmpExtensionSpec,
) -> Result<Vec<bool>, SlmpError> {
Self::validate_direct_bit_read(device.device)?;
let extension = Self::resolve_effective_extension(device, extension);
let payload =
self.build_read_write_payload_extended(device.device, points, None, extension, true);
let sub = if extension.direct_memory_specification == 0xF9 {
0x0081
} else if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0081
} else {
0x0083
};
let data = self
.request(SlmpCommand::DeviceRead, sub, &payload, true)
.await?;
Self::unpack_bit_values(&data, points as usize)
}
async fn write_bits_extended(
&mut self,
device: SlmpQualifiedDeviceAddress,
values: &[bool],
extension: SlmpExtensionSpec,
) -> Result<(), SlmpError> {
Self::validate_direct_bit_write(device.device)?;
let extension = Self::resolve_effective_extension(device, extension);
let words: Vec<u16> = values.iter().map(|value| u16::from(*value)).collect();
let payload = self.build_read_write_payload_extended(
device.device,
values.len() as u16,
Some(&words),
extension,
true,
);
let sub = if extension.direct_memory_specification == 0xF9 {
0x0081
} else if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0081
} else {
0x0083
};
let _ = self
.request(SlmpCommand::DeviceWrite, sub, &payload, true)
.await?;
Ok(())
}
async fn read_random(
&mut self,
word_devices: &[SlmpDeviceAddress],
dword_devices: &[SlmpDeviceAddress],
) -> Result<SlmpRandomReadResult, SlmpError> {
Self::validate_random_read_devices(word_devices, dword_devices)?;
if word_devices.len() > 0xFF || dword_devices.len() > 0xFF {
return Err(SlmpError::new("random counts must be <= 255"));
}
let spec_size = device_spec_size(self.options.compatibility_mode);
let mut payload = vec![word_devices.len() as u8, dword_devices.len() as u8];
payload.resize(
2 + (word_devices.len() + dword_devices.len()) * spec_size,
0,
);
let mut offset = 2;
for device in word_devices {
offset += self.encode_device_spec(*device, &mut payload[offset..]);
}
for device in dword_devices {
offset += self.encode_device_spec(*device, &mut payload[offset..]);
}
let sub = if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0000
} else {
0x0002
};
let data = self
.request(SlmpCommand::DeviceReadRandom, sub, &payload, true)
.await?;
let expected = word_devices.len() * 2 + dword_devices.len() * 4;
if data.len() != expected {
return Err(SlmpError::new(format!(
"read_random response size mismatch expected={expected} actual={}",
data.len()
)));
}
let mut cursor = 0;
let mut result = SlmpRandomReadResult::default();
for _ in 0..word_devices.len() {
result
.word_values
.push(u16::from_le_bytes([data[cursor], data[cursor + 1]]));
cursor += 2;
}
for _ in 0..dword_devices.len() {
result.dword_values.push(u32::from_le_bytes([
data[cursor],
data[cursor + 1],
data[cursor + 2],
data[cursor + 3],
]));
cursor += 4;
}
Ok(result)
}
async fn write_random_words(
&mut self,
word_entries: &[(SlmpDeviceAddress, u16)],
dword_entries: &[(SlmpDeviceAddress, u32)],
) -> Result<(), SlmpError> {
Self::validate_random_write_word_devices(word_entries)?;
if word_entries.len() > 0xFF || dword_entries.len() > 0xFF {
return Err(SlmpError::new("random counts must be <= 255"));
}
let spec_size = device_spec_size(self.options.compatibility_mode);
let size =
2 + (word_entries.len() * (spec_size + 2)) + (dword_entries.len() * (spec_size + 4));
let mut payload = vec![0; size];
payload[0] = word_entries.len() as u8;
payload[1] = dword_entries.len() as u8;
let mut offset = 2;
for (device, value) in word_entries {
offset += self.encode_device_spec(*device, &mut payload[offset..]);
payload[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
offset += 2;
}
for (device, value) in dword_entries {
offset += self.encode_device_spec(*device, &mut payload[offset..]);
payload[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
offset += 4;
}
let sub = if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0000
} else {
0x0002
};
let _ = self
.request(SlmpCommand::DeviceWriteRandom, sub, &payload, true)
.await?;
Ok(())
}
async fn write_random_bits(
&mut self,
bit_entries: &[(SlmpDeviceAddress, bool)],
) -> Result<(), SlmpError> {
if bit_entries.len() > 0xFF {
return Err(SlmpError::new("random bit count must be <= 255"));
}
let spec_size = device_spec_size(self.options.compatibility_mode);
let bit_value_size = if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
1
} else {
2
};
let mut payload = vec![0; 1 + (bit_entries.len() * (spec_size + bit_value_size))];
payload[0] = bit_entries.len() as u8;
let mut offset = 1;
for (device, value) in bit_entries {
offset += self.encode_device_spec(*device, &mut payload[offset..]);
if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
payload[offset] = u8::from(*value);
offset += 1;
} else {
let raw = u16::from(*value).to_le_bytes();
payload[offset..offset + 2].copy_from_slice(&raw);
offset += 2;
}
}
let sub = if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0001
} else {
0x0003
};
let _ = self
.request(SlmpCommand::DeviceWriteRandom, sub, &payload, true)
.await?;
Ok(())
}
async fn read_block(
&mut self,
word_blocks: &[SlmpBlockRead],
bit_blocks: &[SlmpBlockRead],
) -> Result<SlmpBlockReadResult, SlmpError> {
Self::validate_no_lcs_lcc_block_read(word_blocks, bit_blocks)?;
if word_blocks.len() > 0xFF || bit_blocks.len() > 0xFF {
return Err(SlmpError::new("block counts must be <= 255"));
}
let spec_size = device_spec_size(self.options.compatibility_mode);
let total_word_points: usize = word_blocks.iter().map(|block| block.points as usize).sum();
let total_bit_points: usize = bit_blocks.iter().map(|block| block.points as usize).sum();
let mut payload = vec![0; 2 + ((word_blocks.len() + bit_blocks.len()) * (spec_size + 2))];
payload[0] = word_blocks.len() as u8;
payload[1] = bit_blocks.len() as u8;
let mut offset = 2;
for block in word_blocks {
offset += self.encode_device_spec(block.device, &mut payload[offset..]);
payload[offset..offset + 2].copy_from_slice(&block.points.to_le_bytes());
offset += 2;
}
for block in bit_blocks {
offset += self.encode_device_spec(block.device, &mut payload[offset..]);
payload[offset..offset + 2].copy_from_slice(&block.points.to_le_bytes());
offset += 2;
}
let sub = if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0000
} else {
0x0002
};
let data = self
.request(SlmpCommand::DeviceReadBlock, sub, &payload, true)
.await?;
let expected = (total_word_points + total_bit_points) * 2;
if data.len() != expected {
return Err(SlmpError::new(format!(
"read_block response size mismatch expected={expected} actual={}",
data.len()
)));
}
let mut cursor = 0;
let mut result = SlmpBlockReadResult::default();
for _ in 0..total_word_points {
result
.word_values
.push(u16::from_le_bytes([data[cursor], data[cursor + 1]]));
cursor += 2;
}
for _ in 0..total_bit_points {
result
.bit_values
.push(u16::from_le_bytes([data[cursor], data[cursor + 1]]));
cursor += 2;
}
Ok(result)
}
async fn write_block(
&mut self,
word_blocks: &[SlmpBlockWrite],
bit_blocks: &[SlmpBlockWrite],
options: SlmpBlockWriteOptions,
) -> Result<(), SlmpError> {
Self::validate_no_lcs_lcc_block_write(word_blocks, bit_blocks)?;
if options.split_mixed_blocks && !word_blocks.is_empty() && !bit_blocks.is_empty() {
self.write_block_once(word_blocks, &[]).await?;
self.write_block_once(&[], bit_blocks).await?;
return Ok(());
}
match self.write_block_once(word_blocks, bit_blocks).await {
Ok(_) => Ok(()),
Err(error)
if options.retry_mixed_on_error
&& !word_blocks.is_empty()
&& !bit_blocks.is_empty()
&& matches!(error.end_code, Some(0xC056 | 0xC061 | 0x414A)) =>
{
self.write_block_once(word_blocks, &[]).await?;
self.write_block_once(&[], bit_blocks).await
}
Err(error) => Err(error),
}
}
async fn write_block_once(
&mut self,
word_blocks: &[SlmpBlockWrite],
bit_blocks: &[SlmpBlockWrite],
) -> Result<(), SlmpError> {
if word_blocks.len() > 0xFF || bit_blocks.len() > 0xFF {
return Err(SlmpError::new("block counts must be <= 255"));
}
let spec_size = device_spec_size(self.options.compatibility_mode);
let total_word_points: usize = word_blocks.iter().map(|block| block.values.len()).sum();
let total_bit_points: usize = bit_blocks.iter().map(|block| block.values.len()).sum();
let mut payload = vec![
0;
2 + ((word_blocks.len() + bit_blocks.len()) * (spec_size + 2))
+ ((total_word_points + total_bit_points) * 2)
];
payload[0] = word_blocks.len() as u8;
payload[1] = bit_blocks.len() as u8;
let mut offset = 2;
for block in word_blocks {
offset += self.encode_device_spec(block.device, &mut payload[offset..]);
let count = block.values.len() as u16;
payload[offset..offset + 2].copy_from_slice(&count.to_le_bytes());
offset += 2;
}
for block in bit_blocks {
offset += self.encode_device_spec(block.device, &mut payload[offset..]);
let count = block.values.len() as u16;
payload[offset..offset + 2].copy_from_slice(&count.to_le_bytes());
offset += 2;
}
for block in word_blocks {
for value in &block.values {
payload[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
offset += 2;
}
}
for block in bit_blocks {
for value in &block.values {
payload[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
offset += 2;
}
}
let sub = if matches!(
self.options.compatibility_mode,
SlmpCompatibilityMode::Legacy
) {
0x0000
} else {
0x0002
};
self.request(SlmpCommand::DeviceWriteBlock, sub, &payload, true)
.await?;
Ok(())
}
async fn remote_run(&mut self, force: bool, clear_mode: u16) -> Result<(), SlmpError> {
let mode = if force { 0x03 } else { 0x01 };
let payload = [
mode,
0x00,
(clear_mode & 0xFF) as u8,
(clear_mode >> 8) as u8,
];
self.request(SlmpCommand::RemoteRun, 0x0000, &payload, true)
.await?;
Ok(())
}
async fn remote_stop(&mut self) -> Result<(), SlmpError> {
self.request(SlmpCommand::RemoteStop, 0x0000, &[0x01, 0x00], true)
.await?;
Ok(())
}
async fn remote_pause(&mut self, force: bool) -> Result<(), SlmpError> {
let mode = if force { 0x03 } else { 0x01 };
self.request(SlmpCommand::RemotePause, 0x0000, &[mode, 0x00], true)
.await?;
Ok(())
}
async fn remote_latch_clear(&mut self) -> Result<(), SlmpError> {
self.request(SlmpCommand::RemoteLatchClear, 0x0000, &[0x01, 0x00], true)
.await?;
Ok(())
}
async fn remote_reset(&mut self, expect_response: bool) -> Result<(), SlmpError> {
self.request(SlmpCommand::RemoteReset, 0x0000, &[], expect_response)
.await?;
Ok(())
}
async fn remote_password_unlock(&mut self, password: &str) -> Result<(), SlmpError> {
let payload = self.encode_password(password)?;
self.request(SlmpCommand::RemotePasswordUnlock, 0x0000, &payload, true)
.await?;
Ok(())
}
async fn remote_password_lock(&mut self, password: &str) -> Result<(), SlmpError> {
let payload = self.encode_password(password)?;
self.request(SlmpCommand::RemotePasswordLock, 0x0000, &payload, true)
.await?;
Ok(())
}
async fn self_test_loopback(&mut self, data: &[u8]) -> Result<Vec<u8>, SlmpError> {
if data.len() > u16::MAX as usize {
return Err(SlmpError::new("loopback payload must be <= 65535 bytes"));
}
let mut payload = Vec::with_capacity(2 + data.len());
payload.extend_from_slice(&(data.len() as u16).to_le_bytes());
payload.extend_from_slice(data);
let response = self
.request(SlmpCommand::SelfTest, 0x0000, &payload, true)
.await?;
if response.len() < 2 {
return Err(SlmpError::new("self_test response too short"));
}
let length = u16::from_le_bytes([response[0], response[1]]) as usize;
if response.len() < length + 2 {
return Err(SlmpError::new("self_test response length mismatch"));
}
Ok(response[2..2 + length].to_vec())
}
async fn memory_read_words(
&mut self,
head_address: u32,
word_length: u16,
) -> Result<Vec<u16>, SlmpError> {
let mut payload = Vec::with_capacity(6);
payload.extend_from_slice(&head_address.to_le_bytes());
payload.extend_from_slice(&word_length.to_le_bytes());
let data = self
.request(SlmpCommand::MemoryRead, 0x0000, &payload, true)
.await?;
if data.len() != word_length as usize * 2 {
return Err(SlmpError::new("memory_read response size mismatch"));
}
Ok(data
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect())
}
async fn memory_write_words(
&mut self,
head_address: u32,
values: &[u16],
) -> Result<(), SlmpError> {
let mut payload = Vec::with_capacity(6 + (values.len() * 2));
payload.extend_from_slice(&head_address.to_le_bytes());
payload.extend_from_slice(&(values.len() as u16).to_le_bytes());
for value in values {
payload.extend_from_slice(&value.to_le_bytes());
}
self.request(SlmpCommand::MemoryWrite, 0x0000, &payload, true)
.await?;
Ok(())
}
async fn extend_unit_read_bytes(
&mut self,
head_address: u32,
byte_length: u16,
module_no: u16,
) -> Result<Vec<u8>, SlmpError> {
let mut payload = Vec::with_capacity(8);
payload.extend_from_slice(&head_address.to_le_bytes());
payload.extend_from_slice(&byte_length.to_le_bytes());
payload.extend_from_slice(&module_no.to_le_bytes());
self.request(SlmpCommand::ExtendUnitRead, 0x0000, &payload, true)
.await
}
async fn extend_unit_read_words(
&mut self,
head_address: u32,
word_length: u16,
module_no: u16,
) -> Result<Vec<u16>, SlmpError> {
let data = self
.extend_unit_read_bytes(head_address, word_length * 2, module_no)
.await?;
Ok(data
.chunks_exact(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.collect())
}
async fn extend_unit_write_words(
&mut self,
head_address: u32,
module_no: u16,
values: &[u16],
) -> Result<(), SlmpError> {
let mut payload = Vec::with_capacity(8 + values.len() * 2);
payload.extend_from_slice(&head_address.to_le_bytes());
payload.extend_from_slice(&((values.len() * 2) as u16).to_le_bytes());
payload.extend_from_slice(&module_no.to_le_bytes());
for value in values {
payload.extend_from_slice(&value.to_le_bytes());
}
self.request(SlmpCommand::ExtendUnitWrite, 0x0000, &payload, true)
.await?;
Ok(())
}
async fn read_array_labels(
&mut self,
points: &[SlmpLabelArrayReadPoint],
abbreviation_labels: &[String],
) -> Result<Vec<SlmpLabelArrayReadResult>, SlmpError> {
let payload = Self::build_label_array_read_payload(points, abbreviation_labels)?;
let data = self
.request(SlmpCommand::LabelArrayRead, 0x0000, &payload, true)
.await?;
Self::parse_array_label_read_response(&data, points.len())
}
async fn write_array_labels(
&mut self,
points: &[SlmpLabelArrayWritePoint],
abbreviation_labels: &[String],
) -> Result<(), SlmpError> {
let payload = Self::build_label_array_write_payload(points, abbreviation_labels)?;
self.request(SlmpCommand::LabelArrayWrite, 0x0000, &payload, true)
.await?;
Ok(())
}
async fn read_random_labels(
&mut self,
labels: &[String],
abbreviation_labels: &[String],
) -> Result<Vec<SlmpLabelRandomReadResult>, SlmpError> {
let payload = Self::build_label_random_read_payload(labels, abbreviation_labels)?;
let data = self
.request(SlmpCommand::LabelReadRandom, 0x0000, &payload, true)
.await?;
Self::parse_label_random_read_response(&data, labels.len())
}
async fn write_random_labels(
&mut self,
points: &[SlmpLabelRandomWritePoint],
abbreviation_labels: &[String],
) -> Result<(), SlmpError> {
let payload = Self::build_label_random_write_payload(points, abbreviation_labels)?;
self.request(SlmpCommand::LabelWriteRandom, 0x0000, &payload, true)
.await?;
Ok(())
}
fn build_label_array_read_payload(
points: &[SlmpLabelArrayReadPoint],
abbreviation_labels: &[String],
) -> Result<Vec<u8>, SlmpError> {
Self::validate_non_empty_u16_count(points.len(), "array label points")?;
Self::validate_u16_count(abbreviation_labels.len(), "abbreviation labels")?;
let mut payload = Vec::new();
payload.extend_from_slice(&(points.len() as u16).to_le_bytes());
payload.extend_from_slice(&(abbreviation_labels.len() as u16).to_le_bytes());
for label in abbreviation_labels {
Self::append_label_name(&mut payload, label)?;
}
for point in points {
Self::append_label_name(&mut payload, &point.label)?;
Self::label_array_data_bytes(point.unit_specification, point.array_data_length)?;
payload.push(point.unit_specification);
payload.push(0x00);
payload.extend_from_slice(&point.array_data_length.to_le_bytes());
}
Ok(payload)
}
fn build_label_array_write_payload(
points: &[SlmpLabelArrayWritePoint],
abbreviation_labels: &[String],
) -> Result<Vec<u8>, SlmpError> {
Self::validate_non_empty_u16_count(points.len(), "array label points")?;
Self::validate_u16_count(abbreviation_labels.len(), "abbreviation labels")?;
let mut payload = Vec::new();
payload.extend_from_slice(&(points.len() as u16).to_le_bytes());
payload.extend_from_slice(&(abbreviation_labels.len() as u16).to_le_bytes());
for label in abbreviation_labels {
Self::append_label_name(&mut payload, label)?;
}
for point in points {
let expected =
Self::label_array_data_bytes(point.unit_specification, point.array_data_length)?;
if point.data.len() != expected {
return Err(SlmpError::new(format!(
"array label write data size mismatch: expected={expected} actual={}",
point.data.len()
)));
}
Self::append_label_name(&mut payload, &point.label)?;
payload.push(point.unit_specification);
payload.push(0x00);
payload.extend_from_slice(&point.array_data_length.to_le_bytes());
payload.extend_from_slice(&point.data);
}
Ok(payload)
}
fn build_label_random_read_payload(
labels: &[String],
abbreviation_labels: &[String],
) -> Result<Vec<u8>, SlmpError> {
Self::validate_non_empty_u16_count(labels.len(), "labels")?;
Self::validate_u16_count(abbreviation_labels.len(), "abbreviation labels")?;
let mut payload = Vec::new();
payload.extend_from_slice(&(labels.len() as u16).to_le_bytes());
payload.extend_from_slice(&(abbreviation_labels.len() as u16).to_le_bytes());
for label in abbreviation_labels {
Self::append_label_name(&mut payload, label)?;
}
for label in labels {
Self::append_label_name(&mut payload, label)?;
}
Ok(payload)
}
fn build_label_random_write_payload(
points: &[SlmpLabelRandomWritePoint],
abbreviation_labels: &[String],
) -> Result<Vec<u8>, SlmpError> {
Self::validate_non_empty_u16_count(points.len(), "random label points")?;
Self::validate_u16_count(abbreviation_labels.len(), "abbreviation labels")?;
let mut payload = Vec::new();
payload.extend_from_slice(&(points.len() as u16).to_le_bytes());
payload.extend_from_slice(&(abbreviation_labels.len() as u16).to_le_bytes());
for label in abbreviation_labels {
Self::append_label_name(&mut payload, label)?;
}
for point in points {
Self::validate_u16_count(point.data.len(), "write data length")?;
Self::append_label_name(&mut payload, &point.label)?;
payload.extend_from_slice(&(point.data.len() as u16).to_le_bytes());
payload.extend_from_slice(&point.data);
}
Ok(payload)
}
fn parse_array_label_read_response(
data: &[u8],
expected_points: usize,
) -> Result<Vec<SlmpLabelArrayReadResult>, SlmpError> {
if data.len() < 2 {
return Err(SlmpError::new("array label read response too short"));
}
let count = u16::from_le_bytes([data[0], data[1]]) as usize;
if count != expected_points {
return Err(SlmpError::new(format!(
"array label read point count mismatch: expected={expected_points} actual={count}"
)));
}
let mut offset = 2usize;
let mut results = Vec::with_capacity(count);
for _ in 0..count {
if offset + 4 > data.len() {
return Err(SlmpError::new(
"array label read response truncated before metadata",
));
}
let data_type_id = data[offset];
let unit_specification = data[offset + 1];
let array_data_length = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
offset += 4;
let data_size = Self::label_array_data_bytes(unit_specification, array_data_length)?;
if offset + data_size > data.len() {
return Err(SlmpError::new(
"array label read response truncated in data payload",
));
}
results.push(SlmpLabelArrayReadResult {
data_type_id,
unit_specification,
array_data_length,
data: data[offset..offset + data_size].to_vec(),
});
offset += data_size;
}
if offset != data.len() {
return Err(SlmpError::new(
"array label read response has trailing bytes",
));
}
Ok(results)
}
fn parse_label_random_read_response(
data: &[u8],
expected_points: usize,
) -> Result<Vec<SlmpLabelRandomReadResult>, SlmpError> {
if data.len() < 2 {
return Err(SlmpError::new("label random read response too short"));
}
let count = u16::from_le_bytes([data[0], data[1]]) as usize;
if count != expected_points {
return Err(SlmpError::new(format!(
"label random read point count mismatch: expected={expected_points} actual={count}"
)));
}
let mut offset = 2usize;
let mut results = Vec::with_capacity(count);
for _ in 0..count {
if offset + 4 > data.len() {
return Err(SlmpError::new(
"label random read response truncated before metadata",
));
}
let data_type_id = data[offset];
let spare = data[offset + 1];
let read_data_length = u16::from_le_bytes([data[offset + 2], data[offset + 3]]);
offset += 4;
let data_size = read_data_length as usize;
if offset + data_size > data.len() {
return Err(SlmpError::new(
"label random read response truncated in data payload",
));
}
results.push(SlmpLabelRandomReadResult {
data_type_id,
spare,
read_data_length,
data: data[offset..offset + data_size].to_vec(),
});
offset += data_size;
}
if offset != data.len() {
return Err(SlmpError::new(
"label random read response has trailing bytes",
));
}
Ok(results)
}
fn append_label_name(payload: &mut Vec<u8>, label: &str) -> Result<(), SlmpError> {
if label.is_empty() {
return Err(SlmpError::new("label must not be empty"));
}
let utf16: Vec<u16> = label.encode_utf16().collect();
Self::validate_u16_count(utf16.len(), "label name length")?;
payload.extend_from_slice(&(utf16.len() as u16).to_le_bytes());
for ch in utf16 {
payload.extend_from_slice(&ch.to_le_bytes());
}
Ok(())
}
fn label_array_data_bytes(
unit_specification: u8,
array_data_length: u16,
) -> Result<usize, SlmpError> {
match unit_specification {
0 => Ok(array_data_length as usize * 2),
1 => Ok(array_data_length as usize),
other => Err(SlmpError::new(format!(
"unit_specification must be 0(word) or 1(byte): {other}"
))),
}
}
fn validate_non_empty_u16_count(count: usize, name: &str) -> Result<(), SlmpError> {
if count == 0 {
return Err(SlmpError::new(format!("{name} must not be empty")));
}
Self::validate_u16_count(count, name)
}
fn validate_u16_count(count: usize, name: &str) -> Result<(), SlmpError> {
if count > u16::MAX as usize {
return Err(SlmpError::new(format!("{name} must be <= 65535")));
}
Ok(())
}
async fn read_long_timer(
&mut self,
head_no: u32,
points: usize,
) -> Result<Vec<SlmpLongTimerResult>, SlmpError> {
let words = self
.read_words_raw(
SlmpDeviceAddress::new(SlmpDeviceCode::LTN, head_no),
(points * 4) as u16,
)
.await?;
Ok(Self::parse_long_timer_words(&words, head_no, "LTN"))
}
async fn read_long_retentive_timer(
&mut self,
head_no: u32,
points: usize,
) -> Result<Vec<SlmpLongTimerResult>, SlmpError> {
let words = self
.read_words_raw(
SlmpDeviceAddress::new(SlmpDeviceCode::LSTN, head_no),
(points * 4) as u16,
)
.await?;
Ok(Self::parse_long_timer_words(&words, head_no, "LSTN"))
}
async fn request(
&mut self,
command: SlmpCommand,
subcommand: u16,
payload: &[u8],
expect_response: bool,
) -> Result<Vec<u8>, SlmpError> {
self.validate_request_payload(command, subcommand, payload)?;
self.build_request_frame(command, subcommand, payload);
let tx_len = self.last_request_frame.len() as u64;
match &mut self.transport {
Transport::Tcp(stream) => {
timeout(
self.options.timeout,
stream.write_all(&self.last_request_frame),
)
.await
.map_err(|_| SlmpError::new("tcp write timed out"))??;
self.traffic_stats.request_count += 1;
self.traffic_stats.tx_bytes += tx_len;
if !expect_response {
self.last_response_frame.clear();
return Ok(Vec::new());
}
Self::receive_tcp_frame(
stream,
self.options.timeout,
&mut self.last_response_frame,
)
.await?;
self.traffic_stats.rx_bytes += self.last_response_frame.len() as u64;
Self::parse_response(command, subcommand, &self.last_response_frame)
}
Transport::Udp(socket) => {
timeout(self.options.timeout, socket.send(&self.last_request_frame))
.await
.map_err(|_| SlmpError::new("udp send timed out"))??;
self.traffic_stats.request_count += 1;
self.traffic_stats.tx_bytes += tx_len;
if !expect_response {
self.last_response_frame.clear();
return Ok(Vec::new());
}
self.last_response_frame.resize(UDP_RECEIVE_BUFFER_SIZE, 0);
let received = timeout(
self.options.timeout,
socket.recv(&mut self.last_response_frame),
)
.await
.map_err(|_| SlmpError::new("udp receive timed out"))??;
self.last_response_frame.truncate(received);
self.traffic_stats.rx_bytes += self.last_response_frame.len() as u64;
Self::parse_response(command, subcommand, &self.last_response_frame)
}
}
}
fn validate_request_payload(
&self,
command: SlmpCommand,
subcommand: u16,
payload: &[u8],
) -> Result<(), SlmpError> {
if matches!(command, SlmpCommand::MonitorRegister) && matches!(subcommand, 0x0000 | 0x0002)
{
Self::validate_plain_monitor_register_payload(
self.options.compatibility_mode,
payload,
)?;
}
Ok(())
}
fn validate_plain_monitor_register_payload(
mode: SlmpCompatibilityMode,
payload: &[u8],
) -> Result<(), SlmpError> {
if payload.len() < 2 {
return Err(SlmpError::new("Monitor register payload is too short."));
}
let word_count = payload[0] as usize;
let dword_count = payload[1] as usize;
let spec_size = device_spec_size(mode);
let expected = 2 + (word_count + dword_count) * spec_size;
if payload.len() != expected {
return Err(SlmpError::new(format!(
"Monitor register payload size mismatch: expected={expected} actual={}",
payload.len()
)));
}
let mut offset = 2usize;
for _ in 0..(word_count + dword_count) {
let code = match mode {
SlmpCompatibilityMode::Legacy => u16::from(payload[offset + 3]),
SlmpCompatibilityMode::Iqr => {
u16::from_le_bytes([payload[offset + 4], payload[offset + 5]])
}
};
if code == SlmpDeviceCode::LCS.as_u16() || code == SlmpDeviceCode::LCC.as_u16() {
return Err(SlmpError::new(
"Entry Monitor Device (0x0801) does not support LCS/LCC. Poll them through read_typed/read_named instead.",
));
}
offset += spec_size;
}
Ok(())
}
fn build_request_frame(&mut self, command: SlmpCommand, subcommand: u16, payload: &[u8]) {
let header_size = match self.options.frame_type {
SlmpFrameType::Frame4E => 19,
SlmpFrameType::Frame3E => 15,
};
self.last_request_frame
.resize(header_size + payload.len(), 0);
let frame = &mut self.last_request_frame;
match self.options.frame_type {
SlmpFrameType::Frame4E => {
frame[0] = 0x54;
frame[1] = 0x00;
frame[2..4].copy_from_slice(&self.serial.to_le_bytes());
self.serial = self.serial.wrapping_add(1);
Self::write_target(&mut frame[6..11], self.options.target);
frame[11..13].copy_from_slice(&(6u16 + payload.len() as u16).to_le_bytes());
frame[13..15].copy_from_slice(&self.options.monitoring_timer.to_le_bytes());
frame[15..17].copy_from_slice(&command.as_u16().to_le_bytes());
frame[17..19].copy_from_slice(&subcommand.to_le_bytes());
}
SlmpFrameType::Frame3E => {
frame[0] = 0x50;
frame[1] = 0x00;
Self::write_target(&mut frame[2..7], self.options.target);
frame[7..9].copy_from_slice(&(6u16 + payload.len() as u16).to_le_bytes());
frame[9..11].copy_from_slice(&self.options.monitoring_timer.to_le_bytes());
frame[11..13].copy_from_slice(&command.as_u16().to_le_bytes());
frame[13..15].copy_from_slice(&subcommand.to_le_bytes());
}
}
frame[header_size..].copy_from_slice(payload);
}
fn write_target(buffer: &mut [u8], target: SlmpTargetAddress) {
buffer[0] = target.network;
buffer[1] = target.station;
buffer[2..4].copy_from_slice(&target.module_io.to_le_bytes());
buffer[4] = target.multidrop;
}
async fn receive_tcp_frame(
stream: &mut TcpStream,
timeout_duration: std::time::Duration,
frame: &mut Vec<u8>,
) -> Result<(), SlmpError> {
let mut header = [0u8; 13];
timeout(timeout_duration, stream.read_exact(&mut header[0..2]))
.await
.map_err(|_| SlmpError::new("tcp read timed out"))??;
if header[0] == 0xD4 && header[1] == 0x00 {
timeout(timeout_duration, stream.read_exact(&mut header[2..13]))
.await
.map_err(|_| SlmpError::new("tcp read timed out"))??;
let length = u16::from_le_bytes([header[11], header[12]]) as usize;
frame.resize(13 + length, 0);
frame[0..13].copy_from_slice(&header);
timeout(timeout_duration, stream.read_exact(&mut frame[13..]))
.await
.map_err(|_| SlmpError::new("tcp read timed out"))??;
return Ok(());
}
if header[0] == 0xD0 && header[1] == 0x00 {
timeout(timeout_duration, stream.read_exact(&mut header[2..9]))
.await
.map_err(|_| SlmpError::new("tcp read timed out"))??;
let length = u16::from_le_bytes([header[7], header[8]]) as usize;
frame.resize(9 + length, 0);
frame[0..9].copy_from_slice(&header[..9]);
timeout(timeout_duration, stream.read_exact(&mut frame[9..]))
.await
.map_err(|_| SlmpError::new("tcp read timed out"))??;
return Ok(());
}
Err(SlmpError::new("invalid response subheader"))
}
fn parse_response(
command: SlmpCommand,
subcommand: u16,
response: &[u8],
) -> Result<Vec<u8>, SlmpError> {
let is_4e = response.len() >= 13 && response[0] == 0xD4 && response[1] == 0x00;
let is_3e = response.len() >= 9 && response[0] == 0xD0 && response[1] == 0x00;
if !is_4e && !is_3e {
return Err(SlmpError::with_context(
"invalid response header",
None,
Some(command),
Some(subcommand),
));
}
let header_size = if is_4e { 13 } else { 9 };
let length_index = if is_4e { 11 } else { 7 };
let data_length =
u16::from_le_bytes([response[length_index], response[length_index + 1]]) as usize;
if response.len() < header_size + data_length || data_length < 2 {
return Err(SlmpError::with_context(
"malformed response",
None,
Some(command),
Some(subcommand),
));
}
let end_index = header_size;
let end_code = u16::from_le_bytes([response[end_index], response[end_index + 1]]);
if end_code != 0 {
return Err(SlmpError::with_context(
format!(
"SLMP error end_code=0x{end_code:04X} command=0x{:04X} subcommand=0x{subcommand:04X}",
command.as_u16()
),
Some(end_code),
Some(command),
Some(subcommand),
));
}
if data_length == 2 {
return Ok(Vec::new());
}
Ok(response[header_size + 2..header_size + data_length].to_vec())
}
fn word_subcommand(&self, bit_unit: bool) -> u16 {
match (self.options.compatibility_mode, bit_unit) {
(SlmpCompatibilityMode::Legacy, false) => 0x0000,
(SlmpCompatibilityMode::Legacy, true) => 0x0001,
(SlmpCompatibilityMode::Iqr, false) => 0x0002,
(SlmpCompatibilityMode::Iqr, true) => 0x0003,
}
}
fn validate_direct_bit_read(device: SlmpDeviceAddress) -> Result<(), SlmpError> {
if Self::is_long_timer_state_device(device.code) {
return Err(SlmpError::new(
"Direct bit read is not supported for long timer state devices. Use read_typed/read_named or a 4-word current-value block read.",
));
}
Ok(())
}
fn validate_direct_bit_write(device: SlmpDeviceAddress) -> Result<(), SlmpError> {
if Self::requires_random_bit_write(device.code) {
return Err(SlmpError::new(
"Direct bit write is not supported for long-family state devices. Use write_typed/write_named so random bit write (0x1402) is selected.",
));
}
Ok(())
}
fn validate_direct_word_read(device: SlmpDeviceAddress, points: u16) -> Result<(), SlmpError> {
match device.code {
code if Self::is_random_dword_only_read_device(code) => Err(SlmpError::new(
"Direct word read is not supported for LCN/LZ. Use read_typed/read_named for 32-bit access.",
)),
code if matches!(code, SlmpDeviceCode::LTN | SlmpDeviceCode::LSTN)
&& (points == 0 || points % 4 != 0) =>
{
Err(SlmpError::new(
"Long timer and long retentive timer current values must be read as 4-word blocks.",
))
}
_ => Ok(()),
}
}
fn validate_direct_word_write(device: SlmpDeviceAddress) -> Result<(), SlmpError> {
if Self::is_long_current_value_device(device.code)
|| Self::is_dword_only_scalar_device(device.code)
{
return Err(SlmpError::new(
"Direct word write is not supported for LTN/LSTN/LCN/LZ. Use write_typed/write_named with ':D' or ':L' instead.",
));
}
Ok(())
}
fn validate_direct_dword_read(device: SlmpDeviceAddress) -> Result<(), SlmpError> {
if Self::is_long_current_value_device(device.code)
|| Self::is_dword_only_scalar_device(device.code)
{
return Err(SlmpError::new(
"Direct dword read is not supported for LTN/LSTN/LCN/LZ. Use read_typed/read_named or the supported long-family helper route.",
));
}
Ok(())
}
fn validate_direct_dword_write(device: SlmpDeviceAddress) -> Result<(), SlmpError> {
if Self::is_long_current_value_device(device.code)
|| Self::is_dword_only_scalar_device(device.code)
{
return Err(SlmpError::new(
"Direct dword write is not supported for LTN/LSTN/LCN/LZ. Use write_typed/write_named so random dword write (0x1402) is selected.",
));
}
Ok(())
}
fn validate_random_read_devices(
word_devices: &[SlmpDeviceAddress],
dword_devices: &[SlmpDeviceAddress],
) -> Result<(), SlmpError> {
for device in word_devices.iter().chain(dword_devices.iter()) {
if Self::is_long_timer_state_device(device.code) {
return Err(SlmpError::new(
"Read Random (0x0403) does not support LTS/LTC/LSTS/LSTC. Use read_typed/read_named or a 4-word current-value block read.",
));
}
if matches!(device.code, SlmpDeviceCode::LCS | SlmpDeviceCode::LCC) {
return Err(SlmpError::new(
"Read Random (0x0403) does not support LCS/LCC. Use read_typed/read_named so direct bit read is selected.",
));
}
}
for device in word_devices {
if Self::is_long_current_value_device(device.code)
|| Self::is_dword_only_scalar_device(device.code)
{
return Err(SlmpError::new(
"Read Random (0x0403) does not support LTN/LSTN/LCN/LZ as word entries. Use dword entries or read_typed/read_named with ':D' or ':L' instead.",
));
}
}
Ok(())
}
fn validate_random_write_word_devices(
word_entries: &[(SlmpDeviceAddress, u16)],
) -> Result<(), SlmpError> {
for (device, _) in word_entries {
if Self::is_long_current_value_device(device.code)
|| Self::is_dword_only_scalar_device(device.code)
{
return Err(SlmpError::new(
"Write Random (0x1402) does not support LTN/LSTN/LCN/LZ as word entries. Use dword entries or write_typed/write_named with ':D' or ':L' instead.",
));
}
}
Ok(())
}
fn is_long_timer_state_device(code: SlmpDeviceCode) -> bool {
matches!(
code,
SlmpDeviceCode::LTS | SlmpDeviceCode::LTC | SlmpDeviceCode::LSTS | SlmpDeviceCode::LSTC
)
}
fn requires_random_bit_write(code: SlmpDeviceCode) -> bool {
Self::is_long_timer_state_device(code)
|| matches!(code, SlmpDeviceCode::LCS | SlmpDeviceCode::LCC)
}
fn is_long_current_value_device(code: SlmpDeviceCode) -> bool {
matches!(
code,
SlmpDeviceCode::LTN | SlmpDeviceCode::LSTN | SlmpDeviceCode::LCN
)
}
fn is_dword_only_scalar_device(code: SlmpDeviceCode) -> bool {
matches!(code, SlmpDeviceCode::LZ)
}
fn is_random_dword_only_read_device(code: SlmpDeviceCode) -> bool {
matches!(code, SlmpDeviceCode::LCN | SlmpDeviceCode::LZ)
}
fn validate_no_lcs_lcc_block_read(
word_blocks: &[SlmpBlockRead],
bit_blocks: &[SlmpBlockRead],
) -> Result<(), SlmpError> {
for block in word_blocks {
if matches!(
block.device.code,
SlmpDeviceCode::LTN | SlmpDeviceCode::LSTN
) && block.points % 4 != 0
{
return Err(SlmpError::new(
"Read Block (0x0406) direct long timer current reads require 4-word blocks.",
));
}
}
for block in word_blocks.iter().chain(bit_blocks.iter()) {
if Self::is_random_dword_only_read_device(block.device.code) {
return Err(SlmpError::new(
"Read Block (0x0406) does not support LCN/LZ as word or bit blocks. Use read_typed/read_named so random dword read is selected.",
));
}
}
for block in word_blocks.iter().chain(bit_blocks.iter()) {
if matches!(block.device.code, SlmpDeviceCode::LCS | SlmpDeviceCode::LCC) {
return Err(SlmpError::new(
"Read Block (0x0406) does not support LCS/LCC. Use read_typed/read_named so direct bit read is selected.",
));
}
}
Ok(())
}
fn validate_no_lcs_lcc_block_write(
word_blocks: &[SlmpBlockWrite],
bit_blocks: &[SlmpBlockWrite],
) -> Result<(), SlmpError> {
for block in word_blocks.iter().chain(bit_blocks.iter()) {
if Self::is_long_current_value_device(block.device.code)
|| Self::is_dword_only_scalar_device(block.device.code)
{
return Err(SlmpError::new(
"Write Block (0x1406) does not support LTN/LSTN/LCN/LZ as word or bit blocks. Use write_typed/write_named with ':D' or ':L' instead.",
));
}
}
for block in word_blocks.iter().chain(bit_blocks.iter()) {
if matches!(block.device.code, SlmpDeviceCode::LCS | SlmpDeviceCode::LCC) {
return Err(SlmpError::new(
"Write Block (0x1406) does not support LCS/LCC. Use write_typed/write_named or other supported write routes.",
));
}
}
Ok(())
}
fn encode_device_spec(&self, device: SlmpDeviceAddress, output: &mut [u8]) -> usize {
match self.options.compatibility_mode {
SlmpCompatibilityMode::Legacy => {
output[0] = (device.number & 0xFF) as u8;
output[1] = ((device.number >> 8) & 0xFF) as u8;
output[2] = ((device.number >> 16) & 0xFF) as u8;
output[3] = device.code.as_u8();
4
}
SlmpCompatibilityMode::Iqr => {
output[0..4].copy_from_slice(&device.number.to_le_bytes());
output[4..6].copy_from_slice(&device.code.as_u16().to_le_bytes());
6
}
}
}
fn build_read_write_payload(
&self,
device: SlmpDeviceAddress,
points: u16,
values: Option<&[u16]>,
bit_unit: bool,
) -> Vec<u8> {
let write_bytes = if let Some(values) = values {
if bit_unit {
values.len().div_ceil(2)
} else {
values.len() * 2
}
} else {
0
};
let mut payload =
vec![0u8; device_spec_size(self.options.compatibility_mode) + 2 + write_bytes];
let mut offset = self.encode_device_spec(device, &mut payload);
payload[offset..offset + 2].copy_from_slice(&points.to_le_bytes());
offset += 2;
if let Some(values) = values {
if bit_unit {
let mut index = 0;
while index < values.len() {
let high = if values[index] != 0 { 0x10 } else { 0x00 };
index += 1;
let low = if index < values.len() && values[index] != 0 {
0x01
} else {
0x00
};
if index < values.len() {
index += 1;
}
payload[offset] = high | low;
offset += 1;
}
} else {
for value in values {
payload[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
offset += 2;
}
}
}
payload
}
fn resolve_effective_extension(
device: SlmpQualifiedDeviceAddress,
extension: SlmpExtensionSpec,
) -> SlmpExtensionSpec {
let mut result = extension;
if let Some(extension_specification) = device.extension_specification {
result.extension_specification = extension_specification;
}
if let Some(direct_memory_specification) = device.direct_memory_specification {
result.direct_memory_specification = direct_memory_specification;
}
result
}
fn build_read_write_payload_extended(
&self,
device: SlmpDeviceAddress,
points: u16,
values: Option<&[u16]>,
extension: SlmpExtensionSpec,
bit_unit: bool,
) -> Vec<u8> {
let mut payload = self.encode_extended_device_spec(device, extension);
payload.extend_from_slice(&points.to_le_bytes());
if let Some(values) = values {
if bit_unit {
let mut index = 0;
while index < values.len() {
let high = if values[index] != 0 { 0x10 } else { 0x00 };
index += 1;
let low = if index < values.len() && values[index] != 0 {
0x01
} else {
0x00
};
if index < values.len() {
index += 1;
}
payload.push(high | low);
}
} else {
for value in values {
payload.extend_from_slice(&value.to_le_bytes());
}
}
}
payload
}
pub(crate) fn encode_extended_device_spec(
&self,
device: SlmpDeviceAddress,
extension: SlmpExtensionSpec,
) -> Vec<u8> {
if extension.direct_memory_specification == 0xF9 {
return vec![
0x00,
0x00,
(device.number & 0xFF) as u8,
((device.number >> 8) & 0xFF) as u8,
((device.number >> 16) & 0xFF) as u8,
device.code.as_u8(),
0x00,
0x00,
(extension.extension_specification & 0xFF) as u8,
0x00,
0xF9,
];
}
let capture_aligned = matches!(device.code, SlmpDeviceCode::G | SlmpDeviceCode::HG)
&& matches!(extension.direct_memory_specification, 0xF8 | 0xFA);
let mut device_spec = vec![0u8; device_spec_size(self.options.compatibility_mode)];
let _ = self.encode_device_spec(device, &mut device_spec);
if capture_aligned {
let mut payload = Vec::with_capacity(2 + device_spec.len() + 1 + 1 + 2 + 1);
payload.push(extension.extension_specification_modification);
payload.push(extension.device_modification_index);
payload.extend_from_slice(&device_spec);
payload.push(extension.device_modification_flags);
payload.push(0x00);
payload.extend_from_slice(&extension.extension_specification.to_le_bytes());
payload.push(extension.direct_memory_specification);
return payload;
}
let mut payload = Vec::with_capacity(2 + 1 + 1 + 1 + device_spec.len() + 1);
payload.extend_from_slice(&extension.extension_specification.to_le_bytes());
payload.push(extension.extension_specification_modification);
payload.push(extension.device_modification_index);
payload.push(extension.device_modification_flags);
payload.extend_from_slice(&device_spec);
payload.push(extension.direct_memory_specification);
payload
}
fn unpack_bit_values(data: &[u8], points: usize) -> Result<Vec<bool>, SlmpError> {
let need = points.div_ceil(2);
if data.len() < need {
return Err(SlmpError::new("read_bits payload size mismatch"));
}
let mut result = Vec::with_capacity(points);
for byte in data.iter().take(need) {
if result.len() < points {
result.push(((byte >> 4) & 0x01) != 0);
}
if result.len() < points {
result.push((byte & 0x01) != 0);
}
}
Ok(result)
}
fn parse_long_timer_words(
words: &[u16],
head_no: u32,
prefix: &str,
) -> Vec<SlmpLongTimerResult> {
let mut result = Vec::with_capacity(words.len() / 4);
for (index, chunk) in words.chunks_exact(4).enumerate() {
let status_word = chunk[2];
let current_value = chunk[0] as u32 | ((chunk[1] as u32) << 16);
result.push(SlmpLongTimerResult {
index: head_no + index as u32,
device: format!("{prefix}{}", head_no + index as u32),
current_value,
contact: (status_word & 0x0002) != 0,
coil: (status_word & 0x0001) != 0,
status_word,
raw_words: chunk.to_vec(),
});
}
result
}
fn encode_password(&self, password: &str) -> Result<Vec<u8>, SlmpError> {
let raw = password.as_bytes();
match self.options.compatibility_mode {
SlmpCompatibilityMode::Iqr => {
if raw.len() < 6 || raw.len() > 32 {
return Err(SlmpError::new("iQ-R password length must be 6..32"));
}
let mut payload = Vec::with_capacity(2 + raw.len());
payload.extend_from_slice(&(raw.len() as u16).to_le_bytes());
payload.extend_from_slice(raw);
Ok(payload)
}
SlmpCompatibilityMode::Legacy => {
if raw.len() < 6 || raw.len() > 8 {
return Err(SlmpError::new("Q/L password length must be 6..8"));
}
let mut payload = vec![0u8; 8];
payload[..raw.len()].copy_from_slice(raw);
Ok(payload)
}
}
}
}
fn decode_cpu_operation_state(status_word: u16) -> SlmpCpuOperationState {
let raw_code = (status_word & 0x000F) as u8;
let status = match raw_code {
0x00 => SlmpCpuOperationStatus::Run,
0x02 => SlmpCpuOperationStatus::Stop,
0x03 => SlmpCpuOperationStatus::Pause,
_ => SlmpCpuOperationStatus::Unknown,
};
SlmpCpuOperationState {
status,
raw_status_word: status_word,
raw_code,
}
}
impl std::fmt::Debug for SlmpClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SlmpClient").finish_non_exhaustive()
}
}
pub fn encode_device_spec(mode: SlmpCompatibilityMode, device: SlmpDeviceAddress) -> Vec<u8> {
let size = device_spec_size(mode);
let mut output = vec![0u8; size];
match mode {
SlmpCompatibilityMode::Legacy => {
output[0] = (device.number & 0xFF) as u8;
output[1] = ((device.number >> 8) & 0xFF) as u8;
output[2] = ((device.number >> 16) & 0xFF) as u8;
output[3] = device.code.as_u8();
}
SlmpCompatibilityMode::Iqr => {
output[0..4].copy_from_slice(&device.number.to_le_bytes());
output[4..6].copy_from_slice(&device.code.as_u16().to_le_bytes());
}
}
output
}