use std::net::{TcpStream, ToSocketAddrs};
use std::net::Shutdown;
use std::time::Duration;
use std::fmt;
use std::io;
use std::io::{Read, Write};
use std::time::Instant;
pub const CT_PG: u16 = 0x0001; pub const CT_OP: u16 = 0x0002; pub const CT_S7: u16 = 0x0003;
pub const S7_AREA_PE: u8 = 0x81; pub const S7_AREA_PA: u8 = 0x82; pub const S7_AREA_MK: u8 = 0x83; pub const S7_AREA_DB: u8 = 0x84;
pub const S7_WL_BIT: u8 = 0x01;
pub const S7_WL_BYTE: u8 = 0x02;
const TS_RES_BIT: u8 = 0x03;
const TS_RES_BYTE: u8 = 0x04;
const TPKT_ISO_LEN: usize = 7; const PDU_LEN_REQ: u16 = 480; const ISO_CR_LEN: usize = 22; const ISO_CONN_REQ: u8 = 0xE0; const ISO_CONN_OK: u8 = 0xD0; const ISO_PN_REQ_LEN: usize = 25; const ISO_PN_RES_LEN: usize = 27; const ISO_ID: u8 = 0x03; const S7_ID: u8 = 0x32;
const READ_REQ_LEN: usize = 31; const READ_RES_LEN: usize = 18; const WRITE_RES_LEN: usize = 15;
const EOT: u8 = 0x80; const RW_RES_OFFSET: usize = 14;
const RES_SUCCESS: u8 = 0xFF;
const RES_INVALID_ADDRESS: u8 = 0x05;
const RES_NOT_FOUND: u8 = 0x0A;
macro_rules! hi_part {
($x:expr) => {
(($x >> 8) & 0xFF) as u8
};
}
macro_rules! lo_part {
($x:expr) => {
($x & 0xFF) as u8
};
}
macro_rules! make_u16 {
($hi:expr, $lo:expr) => {
((($hi as u16) << 8) | ($lo as u16))
};
}
#[derive(Debug)]
pub enum S7Error {
Io(io::Error),
NotConnected,
TcpConnectionFailed,
ConnectionClosed,
IsoConnectionFailed,
IsoFragmentedPacket,
IsoInvalidHeader,
IsoInvalidTelegram,
PduNegotiationFailed,
InvalidFunParameter,
S7NotFound,
S7InvalidAddress,
S7Unspecified,
Other(String),
}
impl fmt::Display for S7Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
S7Error::Io(e) => write!(f, "IO error: {}", e),
S7Error::NotConnected => write!(f, "Not connected"),
S7Error::TcpConnectionFailed => write!(f, "TCP connection failed"),
S7Error::ConnectionClosed => write!(f, "TCP connection closed by the peer"),
S7Error::IsoConnectionFailed => write!(f, "ISO-on-TCP connection failed"),
S7Error::IsoFragmentedPacket => write!(f, "Fragmented ISO Packet"),
S7Error::IsoInvalidHeader => write!(f, "Invalid ISO Header"),
S7Error::IsoInvalidTelegram => write!(f, "Invalid ISO Telegram"),
S7Error::PduNegotiationFailed => write!(f, "S7 PDU negotiation failed"),
S7Error::InvalidFunParameter => write!(f, "Invalid parameter supplied to the function"),
S7Error::S7NotFound => write!(f, "S7 Resource not found in the CPU"),
S7Error::S7InvalidAddress => write!(f, "S7 Invalid address"),
S7Error::S7Unspecified => write!(f, "S7 unspecified error"),
S7Error::Other(msg) => write!(f, "{}", msg),
}
}
}
impl From<io::Error> for S7Error {
fn from(err: io::Error) -> S7Error {
S7Error::Io(err)
}
}
pub struct S7Client {
stream: Option<TcpStream>,
port: u16,
co_timeout_ms: u64,
rd_timeout_ms: u64,
wr_timeout_ms: u64,
conn_type: u16,
max_rd_pdu_data: u16, max_wr_pdu_data: u16, pub pdu_length: u16,
pub connected: bool,
pub last_time: f64,
pub chunks: usize,
}
fn check_iso_packet(pdu_length: u16, iso_packet: &mut [u8; TPKT_ISO_LEN]) -> Result<usize, S7Error> {
if iso_packet[0] != ISO_ID || iso_packet[4] != 0x02 || iso_packet[5] != 0xF0 {
return Err(S7Error::IsoInvalidHeader);
}
if iso_packet[6] != EOT {
return Err(S7Error::IsoFragmentedPacket);
}
let telegram_length: usize = make_u16!(iso_packet[2], iso_packet[3]) as usize;
if telegram_length < TPKT_ISO_LEN ||
telegram_length - TPKT_ISO_LEN > pdu_length as usize ||
telegram_length - TPKT_ISO_LEN == 0 {
return Err(S7Error::IsoInvalidTelegram);
}
Ok(telegram_length - TPKT_ISO_LEN)
}
impl S7Client {
pub fn new() -> Self {
S7Client {
stream: None,
port: 102,
co_timeout_ms: 3000,
rd_timeout_ms: 1000,
wr_timeout_ms: 500,
conn_type: CT_PG,
max_rd_pdu_data: 0,
max_wr_pdu_data: 0,
pdu_length: 0x0000,
connected: false,
last_time: 0.0,
chunks:0,
}
}
fn check_area(&mut self, area: u8) -> Result<(), S7Error> {
const AREAS: [u8; 4] = [S7_AREA_PE, S7_AREA_PA, S7_AREA_MK, S7_AREA_DB];
if !AREAS.contains(&area) {
return Err(S7Error::InvalidFunParameter);
}
Ok(())
}
pub fn set_connection_type(&mut self, connection_type: u16) -> Result<(), S7Error> {
if connection_type < CT_PG || connection_type > CT_S7 {
return Err(S7Error::InvalidFunParameter);
}
self.conn_type = connection_type;
Ok(())
}
pub fn set_timeout(&mut self, co_timeout_ms: u64, rd_timeout_ms: u64, wr_timeout_ms: u64 ) -> Result<(), S7Error> {
if co_timeout_ms == 0 || rd_timeout_ms == 0 || wr_timeout_ms == 0 {
return Err(S7Error::InvalidFunParameter);
}
self.co_timeout_ms = co_timeout_ms;
self.rd_timeout_ms = rd_timeout_ms;
self.wr_timeout_ms = wr_timeout_ms;
Ok(())
}
pub fn set_connection_port(&mut self, port: u16) -> Result<(), S7Error> {
if port == 0 {
return Err(S7Error::InvalidFunParameter);
}
self.port = port;
Ok(())
}
pub fn connect_s71200_1500(&mut self, ip: &str) -> Result<(), S7Error> {
self.connect_rack_slot(ip, 0, 0)
}
pub fn connect_s7300(&mut self, ip: &str) -> Result<(), S7Error> {
self.connect_rack_slot(ip, 0, 2)
}
pub fn connect_rack_slot(&mut self, ip: &str, rack: u16, slot: u16) -> Result<(), S7Error> {
let local_tsap: u16 = 0x0100;
let remote_tsap: u16 = (self.conn_type << 8) + (rack * 0x20) + slot;
self.connect_tsap(ip, local_tsap, remote_tsap)
}
pub fn connect_tsap(&mut self, ip: &str, local_tsap: u16, remote_tsap: u16) -> Result<(), S7Error> {
self.connected = false;
self.last_time = 0.0;
let start_time = Instant::now();
let addr = format!("{}:{}", ip, self.port);
let co_timeout = Duration::from_millis(self.co_timeout_ms);
let rd_timeout = Duration::from_millis(self.rd_timeout_ms);
let wr_timeout = Duration::from_millis(self.wr_timeout_ms);
let mut stream = TcpStream::connect_timeout(&addr.to_socket_addrs()?.next().ok_or(S7Error::TcpConnectionFailed)?, co_timeout)?;
stream.set_read_timeout(Some(rd_timeout))?;
stream.set_write_timeout(Some(wr_timeout))?;
stream.set_nodelay(true)?;
let iso_cr: [u8; ISO_CR_LEN] = [
ISO_ID, 0x00, hi_part!(ISO_CR_LEN), lo_part!(ISO_CR_LEN), 0x11, ISO_CONN_REQ, 0x00, 0x00, 0x00, 0x01, 0x00, 0xC0, 0x01, 0x0A, 0xC1, 0x02, hi_part!(local_tsap), lo_part!(local_tsap), 0xC2, 0x02, hi_part!(remote_tsap), lo_part!(remote_tsap) ];
stream.write_all(&iso_cr)?;
let mut iso_resp = [0u8; ISO_CR_LEN];
let size_resp = stream.read(&mut iso_resp)?;
if size_resp < ISO_CR_LEN || iso_resp[5] != ISO_CONN_OK {
return Err(S7Error::IsoConnectionFailed);
}
let s7_pn: [u8; ISO_PN_REQ_LEN] = [
ISO_ID,
0x00,
0x00, 0x19,
0x02, 0xf0, 0x80,
S7_ID, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x08, 0x00,
0x00, 0xf0, 0x00, 0x00, 0x01, 0x00, 0x01,
hi_part!(PDU_LEN_REQ),
lo_part!(PDU_LEN_REQ)
];
stream.write_all(&s7_pn)?;
let mut pn_resp = [0u8; ISO_PN_RES_LEN];
let size_pn = stream.read(&mut pn_resp)?;
if size_pn < ISO_PN_RES_LEN || pn_resp[0] != ISO_ID || pn_resp[7] != S7_ID || pn_resp[17] != 0x00 {
return Err(S7Error::PduNegotiationFailed);
}
self.pdu_length = make_u16!(pn_resp[25], pn_resp[26]);
if self.pdu_length == 0 {
return Err(S7Error::PduNegotiationFailed);
}
self.max_rd_pdu_data = self.pdu_length - 18; self.max_wr_pdu_data = self.pdu_length - 28;
self.stream = Some(stream);
self.connected = true;
self.last_time = start_time.elapsed().as_secs_f64() * 1000.0;
Ok(())
}
pub fn disconnect(&mut self) {
if self.connected {
let stream = self.stream.as_mut().unwrap();
let _ = stream.shutdown(Shutdown::Both);
self.stream = None;
self.connected = false;
}
}
pub fn read_area(&mut self, area: u8, db_number: u16, start: u16, wordlen: u8, buffer: &mut [u8]) -> Result<(), S7Error> {
self.last_time = 0.0;
self.chunks = 0;
let _ = self.check_area(area)?;
if wordlen != S7_WL_BIT && wordlen != S7_WL_BYTE {
return Err(S7Error::InvalidFunParameter);
}
if !self.connected {
return Err(S7Error::NotConnected);
}
let start_time = Instant::now();
let datasize: u16 = if wordlen == S7_WL_BYTE {
buffer.len().min(u16::MAX as usize) as u16
} else {
1 };
let stream = self.stream.as_mut().unwrap();
let mut offset = 0;
let mut long_start: u32 = start as u32;
while offset < datasize {
let remaining = datasize - offset;
let chunk_size = remaining.min(self.max_rd_pdu_data);
self.chunks+=1;
let mut request: [u8; READ_REQ_LEN] = [
ISO_ID, 0x00, 0x00, 0x1f, 0x02, 0xf0, 0x80, S7_ID, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x04, 0x01, 0x12, 0x0a, 0x10, wordlen, hi_part!(chunk_size), lo_part!(chunk_size), hi_part!(db_number), lo_part!(db_number), area, 0x00, 0x00, 0x00 ];
let address = if wordlen == S7_WL_BIT {
long_start
} else {
long_start << 3
};
request[28] = ((address >> 16) & 0xFF) as u8;
request[29] = ((address >> 8) & 0xFF) as u8;
request[30] = (address & 0xFF) as u8;
stream.write_all(&request)?;
let mut iso_packet = [0u8; TPKT_ISO_LEN];
stream.read_exact(&mut iso_packet)?;
let s7_comm_size = check_iso_packet(self.pdu_length, &mut iso_packet)?;
if s7_comm_size < READ_RES_LEN {
return Err(S7Error::IsoInvalidTelegram);
}
let mut response = [0u8; PDU_LEN_REQ as usize];
let size_resp = stream.read(&mut response)?;
if size_resp < s7_comm_size {
return Err(S7Error::IsoInvalidTelegram);
}
if response[RW_RES_OFFSET] != RES_SUCCESS {
match response[RW_RES_OFFSET] {
RES_NOT_FOUND => return Err(S7Error::S7NotFound),
RES_INVALID_ADDRESS => return Err(S7Error::S7InvalidAddress),
_ => return Err(S7Error::S7Unspecified)
}
}
let payload = &response[READ_RES_LEN..READ_RES_LEN + (size_resp - READ_RES_LEN).min(chunk_size as usize)];
buffer[offset as usize..offset as usize + payload.len()].copy_from_slice(payload);
offset += chunk_size;
long_start += chunk_size as u32;
}
self.last_time = start_time.elapsed().as_secs_f64() * 1000.0;
Ok(())
}
pub fn write_area(&mut self, area: u8, db_number: u16, start: u16, wordlen: u8, buffer: &[u8]) -> Result<(), S7Error> {
self.last_time = 0.0;
self.chunks = 0;
let _ = self.check_area(area)?;
if wordlen != S7_WL_BIT && wordlen != S7_WL_BYTE {
return Err(S7Error::InvalidFunParameter);
}
if !self.connected {
return Err(S7Error::NotConnected);
}
let start_time = Instant::now();
let stream = self.stream.as_mut().unwrap();
let mut offset = 0;
let mut long_start: u32 = start as u32;
let datasize: usize = if wordlen == S7_WL_BYTE {
buffer.len().min(u16::MAX as usize)
} else {
1 };
let transport: u8 = if wordlen == S7_WL_BIT { TS_RES_BIT } else { TS_RES_BYTE };
while offset < datasize{
self.chunks+=1;
let chunk_size = (datasize - offset).min(self.max_wr_pdu_data as usize);
let chunk = &buffer[offset..offset + chunk_size];
let bits_payload: u16 = if wordlen == S7_WL_BIT { 1 } else { (chunk_size << 3) as u16 };
let mut request = vec![
ISO_ID, 0x00, 0x00, 0x00, 0x02, 0xf0, 0x80, S7_ID, 0x01, 0x00, 0x00, 0x05, 0x00, 0x00, 0x0e, hi_part!(chunk_size + 4), lo_part!(chunk_size + 4), 0x05, 0x01, 0x12, 0x0a, 0x10, wordlen,
hi_part!(chunk_size), lo_part!(chunk_size), hi_part!(db_number), lo_part!(db_number), area, 0x00, 0x00, 0x00, 0x00, transport, hi_part!(bits_payload), lo_part!(bits_payload) ];
request.extend_from_slice(chunk);
let total_len = request.len();
request[2] = hi_part!(total_len);
request[3] = lo_part!(total_len);
let address = if wordlen == S7_WL_BIT {
long_start
} else {
long_start << 3
};
request[28] = ((address >> 16) & 0xFF) as u8;
request[29] = ((address >> 8) & 0xFF) as u8;
request[30] = (address & 0xFF) as u8;
stream.write_all(&request)?;
let mut iso_packet = [0u8; TPKT_ISO_LEN];
stream.read_exact(&mut iso_packet)?;
let s7_comm_size = check_iso_packet(self.pdu_length, &mut iso_packet)?;
if s7_comm_size < WRITE_RES_LEN {
return Err(S7Error::IsoInvalidTelegram);
}
let mut response = [0u8; PDU_LEN_REQ as usize];
let size_resp = stream.read(&mut response)?;
if size_resp < s7_comm_size {
return Err(S7Error::IsoInvalidTelegram);
}
if response[RW_RES_OFFSET] != RES_SUCCESS {
match response[RW_RES_OFFSET] {
RES_NOT_FOUND => return Err(S7Error::S7NotFound),
RES_INVALID_ADDRESS => return Err(S7Error::S7InvalidAddress),
_ => return Err(S7Error::S7Unspecified)
}
}
offset += chunk_size;
long_start += chunk_size as u32;
}
self.last_time = start_time.elapsed().as_secs_f64() * 1000.0;
Ok(())
}
pub fn read_db(&mut self, db_number: u16, start: u16, buffer: &mut [u8]) -> Result<(), S7Error> {
self.read_area(S7_AREA_DB, db_number, start, S7_WL_BYTE, buffer)
}
pub fn read_bit(&mut self, area: u8, db_number: u16, byte_num: u16, bit_idx: u8) -> Result<bool, S7Error> {
if bit_idx > 7 {
return Err(S7Error::InvalidFunParameter);
}
let start: u16 = byte_num * 8 + bit_idx as u16;
let mut buffer = [0u8; 1];
self.read_area(area, db_number, start, S7_WL_BIT, &mut buffer)?;
Ok(buffer[0] != 0)
}
pub fn write_db(&mut self, db_number: u16, start: u16, buffer: &[u8]) -> Result<(), S7Error> {
self.write_area(S7_AREA_DB, db_number, start, S7_WL_BYTE, buffer)
}
pub fn write_bit(&mut self, area: u8, db_number: u16, byte_num: u16, bit_idx: u8, value: bool) -> Result<(), S7Error> {
if bit_idx > 7 {
return Err(S7Error::InvalidFunParameter);
}
let start: u16 = byte_num * 8 + bit_idx as u16;
let mut data = [0u8; 1];
data[0] = value as u8;
self.write_area(area, db_number, start, S7_WL_BIT, &mut data)
}
}
impl Drop for S7Client {
fn drop(&mut self) {
self.disconnect();
}
}