├── .github └── FUNDING.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md └── src ├── client.rs ├── constant.rs ├── error.rs ├── field.rs ├── lib.rs ├── tcp.rs └── transport.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: petar_dambovaliev 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea/ 5 | tests/ -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "s7" 3 | version = "0.1.9" 4 | authors = ["Petar Dambovaliev "] 5 | edition = "2018" 6 | description = "A simple library that can be used to communicate with Siemens S7 family PLC devices" 7 | keywords = ["siemens", "s7", "plc", "simatic"] 8 | license-file = "LICENSE.md" 9 | repository = "https://github.com/petar-dambovaliev/s7" 10 | readme = "README.md" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | byteorder = "1.3.2" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, dambovaliev 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # s7 2 | A simple library that can be used to communicate with Siemens S7 family PLC devices 3 | 4 | This crate provides communication tools for Siemens s7 family devices 5 | So far only `PG.db_read` and `PG.db_write` have been tested on actual hardware 6 | The crate is unstable as of now and provides no guarantees 7 | # examples 8 | ```rust 9 | extern crate s7; 10 | 11 | use s7::{client::Client, field::Bool, field::Fields, field::Float, tcp, transport::Connection}; 12 | use std::net::{IpAddr, Ipv4Addr}; 13 | use std::time::Duration; 14 | 15 | fn main() { 16 | let addr = Ipv4Addr::new(127, 0, 0, 1); 17 | let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, Connection::PG); 18 | 19 | opts.read_timeout = Duration::from_secs(2); 20 | opts.write_timeout = Duration::from_secs(2); 21 | 22 | let t = tcp::Transport::connect(opts).unwrap(); 23 | let mut cl = Client::new(t).unwrap(); 24 | 25 | let buffer = &mut vec![0u8; Bool::size() as usize]; 26 | let db = 888; 27 | 28 | // the offset in the PLC is represented by a float 29 | // the difit on the left is the index within the block 30 | // the digit after the decimal point is only important for the `Bool` to be able to change the relevant bit 31 | // we don't need after 32 | let mut offset = 8.4; 33 | 34 | // Since this is a boolean field, we are going to get back 1 byte 35 | cl.ag_read(db, offset as i32, Bool::size(), buffer).unwrap(); 36 | 37 | // field mod provides types to handle the data from the PLC 38 | // create a bool field from the byte we got 39 | let mut lights = Bool::new(db, offset, buffer.to_vec()).unwrap(); 40 | 41 | // the bit in the byte is set without changing any of the other bits 42 | lights.set_value(!lights.value()); // toggle the light switch 43 | 44 | offset = 12.0; 45 | let mut cooling_buffer = vec![0u8; Float::size() as usize]; 46 | cl.ag_read(db, offset as i32, Float::size(), cooling_buffer.as_mut()) 47 | .unwrap(); 48 | let mut cooling = Float::new(db, offset, cooling_buffer).unwrap(); 49 | cooling.set_value(121.3); 50 | 51 | let fields: Fields = vec![Box::new(lights), Box::new(cooling)]; 52 | 53 | // save back the changed values 54 | for field in fields.iter() { 55 | cl.ag_write( 56 | field.data_block(), 57 | field.offset(), 58 | field.to_bytes().len() as i32, 59 | field.to_bytes().as_mut(), 60 | ) 61 | .unwrap(); 62 | } 63 | } 64 | ``` 65 | # License 66 | 67 | Copyright 2019 Petar Dambovaliev. All rights reserved. 68 | This software may be modified and distributed under the terms 69 | of the BSD license. See the LICENSE file for details. 70 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Petar Dambovaliev. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | use super::constant::{self, Area}; 6 | use super::error::{self, Error}; 7 | use super::transport::{self, Transport}; 8 | use crate::constant::CpuStatus; 9 | use byteorder::{BigEndian, ByteOrder}; 10 | use std::str; 11 | 12 | #[derive(Debug, Clone)] 13 | pub struct CpuInfo { 14 | module_type_name: String, 15 | serial_number: String, 16 | as_name: String, 17 | copyright: String, 18 | module_name: String, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct CPInfo { 23 | max_pdu_length: u16, 24 | max_connections: u16, 25 | max_mpi_rate: u16, 26 | max_bus_rate: u16, 27 | } 28 | 29 | #[derive(Debug, Clone)] 30 | pub struct Client { 31 | transport: T, 32 | } 33 | 34 | impl Client { 35 | pub fn new(mut transport: T) -> Result, Error> { 36 | transport.negotiate()?; 37 | Ok(Client { transport }) 38 | } 39 | 40 | /// # Examples 41 | /// 42 | /// ```no_run 43 | /// use std::net::{Ipv4Addr, IpAddr}; 44 | /// use s7::{client, tcp, transport}; 45 | /// use std::time::Duration; 46 | /// use s7::field::{Bool, Field}; 47 | /// 48 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 49 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 50 | /// 51 | /// opts.read_timeout = Duration::from_secs(2); 52 | /// opts.write_timeout = Duration::from_secs(2); 53 | /// 54 | /// let t = tcp::Transport::connect(opts).unwrap(); 55 | /// let mut cl = client::Client::new(t).unwrap(); 56 | /// 57 | /// let buffer = &mut vec![0u8; Bool::size() as usize]; 58 | /// let db = 888; 59 | /// let offset = 8.4; 60 | /// 61 | /// cl.ag_read(db, offset as i32, Bool::size(), buffer).unwrap(); 62 | /// 63 | /// let mut lights = Bool::new(db, offset, buffer.to_vec()).unwrap(); 64 | /// lights.set_value(!lights.value()); // toggle the light switch 65 | /// 66 | /// // save 67 | /// cl.ag_write( 68 | /// lights.data_block(), 69 | /// lights.offset(), 70 | /// Bool::size(), 71 | /// lights.to_bytes().as_mut() 72 | /// ).unwrap(); 73 | /// 74 | /// ``` 75 | pub fn ag_read( 76 | &mut self, 77 | db_number: i32, 78 | start: i32, 79 | size: i32, 80 | buffer: &mut Vec, 81 | ) -> Result<(), Error> { 82 | return self.read( 83 | Area::DataBausteine, 84 | db_number, 85 | start, 86 | size, 87 | constant::WL_BYTE, 88 | buffer, 89 | ); 90 | } 91 | 92 | /// # Examples 93 | /// 94 | /// ```no_run 95 | /// use std::net::{Ipv4Addr, IpAddr}; 96 | /// use s7::{client, tcp, transport}; 97 | /// use std::time::Duration; 98 | /// use s7::field::{Bool, Field}; 99 | /// 100 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 101 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 102 | /// 103 | /// opts.read_timeout = Duration::from_secs(2); 104 | /// opts.write_timeout = Duration::from_secs(2); 105 | /// 106 | /// let t = tcp::Transport::connect(opts).unwrap(); 107 | /// let mut cl = client::Client::new(t).unwrap(); 108 | /// 109 | /// let buffer = &mut vec![0u8; Bool::size() as usize]; 110 | /// let db = 888; 111 | /// let offset = 8.4; 112 | /// 113 | /// cl.ag_read(db, offset as i32, Bool::size(), buffer).unwrap(); 114 | /// 115 | /// let mut lights = Bool::new(db, offset, buffer.to_vec()).unwrap(); 116 | /// lights.set_value(!lights.value()); // toggle the light switch 117 | /// 118 | /// // save 119 | /// cl.ag_write( 120 | /// lights.data_block(), 121 | /// lights.offset(), 122 | /// Bool::size(), 123 | /// lights.to_bytes().as_mut() 124 | /// ).unwrap(); 125 | /// 126 | /// ``` 127 | pub fn ag_write( 128 | &mut self, 129 | db_number: i32, 130 | start: i32, 131 | size: i32, 132 | buffer: &mut Vec, 133 | ) -> Result<(), Error> { 134 | return self.write( 135 | Area::DataBausteine, 136 | db_number, 137 | start, 138 | size, 139 | constant::WL_BYTE, 140 | buffer, 141 | ); 142 | } 143 | 144 | /// # Examples 145 | /// 146 | /// ```no_run 147 | /// use std::net::{Ipv4Addr, IpAddr}; 148 | /// use s7::{client, tcp, transport}; 149 | /// use std::time::Duration; 150 | /// 151 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 152 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 153 | /// 154 | /// opts.read_timeout = Duration::from_secs(2); 155 | /// opts.write_timeout = Duration::from_secs(2); 156 | /// 157 | /// 158 | /// let t = tcp::Transport::connect(opts).unwrap(); 159 | /// let mut cl = client::Client::new(t).unwrap(); 160 | /// 161 | /// let buffer = &mut vec![0u8; 255]; 162 | /// 163 | /// cl.mb_read(1, 3, buffer).unwrap(); 164 | /// ``` 165 | pub fn mb_read(&mut self, start: i32, size: i32, buffer: &mut Vec) -> Result<(), Error> { 166 | return self.read(Area::Merker, 0, start, size, constant::WL_BYTE, buffer); 167 | } 168 | 169 | /// # Examples 170 | /// 171 | /// ```no_run 172 | /// use std::net::{Ipv4Addr, IpAddr}; 173 | /// use s7::{client, tcp, transport}; 174 | /// use std::time::Duration; 175 | /// 176 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 177 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 178 | /// 179 | /// opts.read_timeout = Duration::from_secs(2); 180 | /// opts.write_timeout = Duration::from_secs(2); 181 | /// 182 | /// 183 | /// let t = tcp::Transport::connect(opts).unwrap(); 184 | /// let mut cl = client::Client::new(t).unwrap(); 185 | /// 186 | /// let buffer = &mut vec![0u8; 255]; 187 | /// 188 | /// cl.mb_write(1, 3, buffer).unwrap(); 189 | /// ``` 190 | pub fn mb_write(&mut self, start: i32, size: i32, buffer: &mut Vec) -> Result<(), Error> { 191 | return self.write(Area::Merker, 0, start, size, constant::WL_BYTE, buffer); 192 | } 193 | 194 | /// # Examples 195 | /// 196 | /// ```no_run 197 | /// use std::net::{Ipv4Addr, IpAddr}; 198 | /// use s7::{client, tcp, transport}; 199 | /// use std::time::Duration; 200 | /// 201 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 202 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 203 | /// 204 | /// opts.read_timeout = Duration::from_secs(2); 205 | /// opts.write_timeout = Duration::from_secs(2); 206 | /// 207 | /// 208 | /// let t = tcp::Transport::connect(opts).unwrap(); 209 | /// let mut cl = client::Client::new(t).unwrap(); 210 | /// 211 | /// let buffer = &mut vec![0u8; 255]; 212 | /// 213 | /// cl.eb_read(1, 3, buffer).unwrap(); 214 | /// ``` 215 | pub fn eb_read(&mut self, start: i32, size: i32, buffer: &mut Vec) -> Result<(), Error> { 216 | return self.read( 217 | Area::ProcessInput, 218 | 0, 219 | start, 220 | size, 221 | constant::WL_BYTE, 222 | buffer, 223 | ); 224 | } 225 | 226 | /// # Examples 227 | /// 228 | /// ```no_run 229 | /// use std::net::{Ipv4Addr, IpAddr}; 230 | /// use s7::{client, tcp, transport}; 231 | /// use std::time::Duration; 232 | /// 233 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 234 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 235 | /// 236 | /// opts.read_timeout = Duration::from_secs(2); 237 | /// opts.write_timeout = Duration::from_secs(2); 238 | /// 239 | /// 240 | /// let t = tcp::Transport::connect(opts).unwrap(); 241 | /// let mut cl = client::Client::new(t).unwrap(); 242 | /// 243 | /// let buffer = &mut vec![0u8; 255]; 244 | /// 245 | /// cl.eb_write(1, 3, buffer).unwrap(); 246 | /// ``` 247 | pub fn eb_write(&mut self, start: i32, size: i32, buffer: &mut Vec) -> Result<(), Error> { 248 | return self.write( 249 | Area::ProcessInput, 250 | 0, 251 | start, 252 | size, 253 | constant::WL_BYTE, 254 | buffer, 255 | ); 256 | } 257 | 258 | /// # Examples 259 | /// 260 | /// ```no_run 261 | /// use std::net::{Ipv4Addr, IpAddr}; 262 | /// use s7::{client, tcp, transport}; 263 | /// use std::time::Duration; 264 | /// 265 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 266 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 267 | /// 268 | /// opts.read_timeout = Duration::from_secs(2); 269 | /// opts.write_timeout = Duration::from_secs(2); 270 | /// 271 | /// 272 | /// let t = tcp::Transport::connect(opts).unwrap(); 273 | /// let mut cl = client::Client::new(t).unwrap(); 274 | /// 275 | /// let buffer = &mut vec![0u8; 255]; 276 | /// 277 | /// cl.ab_read(1, 3, buffer).unwrap(); 278 | /// ``` 279 | pub fn ab_read(&mut self, start: i32, size: i32, buffer: &mut Vec) -> Result<(), Error> { 280 | return self.read( 281 | Area::ProcessOutput, 282 | 0, 283 | start, 284 | size, 285 | constant::WL_BYTE, 286 | buffer, 287 | ); 288 | } 289 | 290 | /// # Examples 291 | /// 292 | /// ```no_run 293 | /// use std::net::{Ipv4Addr, IpAddr}; 294 | /// use s7::{client, tcp, transport}; 295 | /// use std::time::Duration; 296 | /// 297 | /// let addr = Ipv4Addr::new(127, 0, 0, 1); 298 | /// let mut opts = tcp::Options::new(IpAddr::from(addr), 5, 5, transport::Connection::PG); 299 | /// 300 | /// opts.read_timeout = Duration::from_secs(2); 301 | /// opts.write_timeout = Duration::from_secs(2); 302 | /// 303 | /// 304 | /// let t = tcp::Transport::connect(opts).unwrap(); 305 | /// let mut cl = client::Client::new(t).unwrap(); 306 | /// 307 | /// let buffer = &mut vec![0u8; 255]; 308 | /// 309 | /// cl.ab_write(1, 3, buffer).unwrap(); 310 | /// ``` 311 | pub fn ab_write(&mut self, start: i32, size: i32, buffer: &mut Vec) -> Result<(), Error> { 312 | return self.write( 313 | Area::ProcessOutput, 314 | 0, 315 | start, 316 | size, 317 | constant::WL_BYTE, 318 | buffer, 319 | ); 320 | } 321 | 322 | //read generic area, pass result into a buffer 323 | fn read( 324 | &mut self, 325 | area: Area, 326 | db_number: i32, 327 | mut start: i32, 328 | mut amount: i32, 329 | mut word_len: i32, 330 | buffer: &mut Vec, 331 | ) -> Result<(), Error> { 332 | // Some adjustment 333 | match area { 334 | Area::Counter => word_len = constant::WL_COUNTER, 335 | Area::Timer => word_len = constant::WL_TIMER, 336 | _ => {} 337 | }; 338 | 339 | // Calc Word size 340 | let mut word_size = constant::data_size_byte(word_len); 341 | 342 | if word_size == 0 { 343 | return Err(Error::Response { 344 | code: error::ISO_INVALID_DATA_SIZE, 345 | }); 346 | } 347 | 348 | if word_len == constant::WL_BIT { 349 | amount = 1; // Only 1 bit can be transferred at time 350 | } else { 351 | if word_len != constant::WL_COUNTER && word_len != constant::WL_TIMER { 352 | amount = amount * word_size; 353 | word_size = 1; 354 | word_len = constant::WL_BYTE; 355 | } 356 | } 357 | 358 | let pdu_length = self.transport.pdu_length(); 359 | 360 | if pdu_length == 0 { 361 | return Err(Error::PduLength(pdu_length)); 362 | } 363 | 364 | let max_elements = (pdu_length - 18) / word_size; // 18 = Reply telegram header //lth note here 365 | 366 | let mut tot_elements = amount; 367 | let db_bytes = (db_number as u16).to_be_bytes(); 368 | let mut offset = 0; 369 | 370 | while tot_elements > 0 { 371 | let mut num_elements = tot_elements; 372 | 373 | if num_elements > max_elements { 374 | num_elements = max_elements; 375 | } 376 | 377 | let size_requested = num_elements * word_size; 378 | // Setup the telegram 379 | let mut request = 380 | transport::READ_WRITE_TELEGRAM[..constant::SIZE_HEADER_READ as usize].to_vec(); 381 | 382 | // Set DB Number 383 | request[25] = db_bytes[0]; 384 | request[26] = db_bytes[1]; 385 | 386 | // Set Area 387 | request[27] = area as u8; 388 | // match area { 389 | // Area::DataBausteine => request[27] = area as u8, 390 | // _ => {} 391 | // } 392 | 393 | // Adjusts Start and word length 394 | let mut address = match word_len { 395 | constant::WL_BIT | constant::WL_COUNTER | constant::WL_TIMER => { 396 | request[22] = word_len as u8; 397 | start 398 | } 399 | _ => start << 3, 400 | }; 401 | 402 | // Num elements 403 | let num_elements_bytes = (num_elements as u16).to_be_bytes(); 404 | request[23] = num_elements_bytes[0]; 405 | request[24] = num_elements_bytes[1]; 406 | 407 | // Address into the PLC (only 3 bytes) 408 | request[30] = (address & 0x0FF) as u8; 409 | address = address >> 8; 410 | request[29] = (address & 0x0FF) as u8; 411 | address = address >> 8; 412 | request[28] = (address & 0x0FF) as u8; 413 | 414 | let result = self.transport.send(request.as_slice()); 415 | 416 | match result { 417 | Ok(response) => { 418 | if response.len() < 25 { 419 | return Err(Error::Response { 420 | code: error::ISO_INVALID_DATA_SIZE, 421 | }); 422 | } 423 | 424 | if response[21] != 0xFF { 425 | return Err(Error::CPU { 426 | code: response[21] as i32, 427 | }); 428 | } 429 | let (mut i, end): (usize, usize) = (25, 25 + (size_requested as usize)); 430 | 431 | //copy response to buffer 432 | for k in offset..size_requested { 433 | if i == end { 434 | break; 435 | } 436 | buffer[k as usize] = response[i]; 437 | i += 1; 438 | } 439 | offset += size_requested; 440 | } 441 | Err(e) => { 442 | return Err(e); 443 | } 444 | } 445 | 446 | tot_elements -= num_elements; 447 | start += num_elements * word_size 448 | } 449 | Ok(()) 450 | } 451 | 452 | fn write( 453 | &mut self, 454 | area: Area, 455 | db_number: i32, 456 | mut start: i32, 457 | mut amount: i32, 458 | mut word_len: i32, 459 | buffer: &mut Vec, 460 | ) -> Result<(), Error> { 461 | // Some adjustment 462 | word_len = match area { 463 | Area::Counter => constant::WL_COUNTER, 464 | Area::Timer => constant::WL_TIMER, 465 | _ => word_len, 466 | }; 467 | 468 | // Calc Word size 469 | let mut word_size = constant::data_size_byte(word_len); 470 | 471 | if word_size == 0 { 472 | return Err(Error::Response { 473 | code: error::ISO_INVALID_DATA_SIZE, 474 | }); 475 | } 476 | 477 | if word_len == constant::WL_BIT { 478 | amount = 1; // Only 1 bit can be transferred at time 479 | } else { 480 | if word_len != constant::WL_COUNTER && word_len != constant::WL_TIMER { 481 | amount = amount * word_size; 482 | word_size = 1; 483 | word_len = constant::WL_BYTE; 484 | } 485 | } 486 | 487 | let mut offset: i32 = 0; 488 | let pdu_length = self.transport.pdu_length(); 489 | let max_elements = (pdu_length - 35) / word_size; // 35 = Reply telegram header 490 | let mut tot_elements = amount; 491 | 492 | while tot_elements > 0 { 493 | let mut num_elements = tot_elements; 494 | if num_elements > max_elements { 495 | num_elements = max_elements; 496 | } 497 | let data_size = num_elements * word_size; 498 | let iso_size = constant::SIZE_HEADER_WRITE + data_size; 499 | 500 | // Setup the telegram 501 | let mut request_data = transport::READ_WRITE_TELEGRAM.to_vec(); 502 | // Whole telegram Size 503 | BigEndian::write_u16(request_data[2..].as_mut(), iso_size as u16); 504 | // Data length 505 | let mut length = data_size + 4; 506 | BigEndian::write_u16(request_data[15..].as_mut(), length as u16); 507 | // Function 508 | request_data[17] = 0x05; 509 | // Set DB Number 510 | request_data[27] = area as u8; 511 | 512 | match area { 513 | Area::DataBausteine => { 514 | BigEndian::write_u16(request_data[25..].as_mut(), db_number as u16) 515 | } 516 | _ => {} 517 | } 518 | // Adjusts start and word length 519 | let mut address = match word_len { 520 | constant::WL_BIT | constant::WL_COUNTER | constant::WL_TIMER => { 521 | length = data_size; 522 | request_data[22] = word_len as u8; 523 | start 524 | } 525 | _ => { 526 | length = data_size << 3; 527 | start << 3 528 | } 529 | }; 530 | 531 | // Num elements 532 | BigEndian::write_u16(request_data[23..].as_mut(), num_elements as u16); 533 | // address into the PLC 534 | request_data[30] = (address & 0x0FF) as u8; 535 | address = address >> 8; 536 | request_data[29] = (address & 0x0FF) as u8; 537 | address = address >> 8; 538 | request_data[28] = (address & 0x0FF) as u8; 539 | 540 | // Transport Size 541 | match word_len { 542 | constant::WL_BIT => request_data[32] = constant::TS_RES_BIT as u8, 543 | constant::WL_COUNTER | constant::WL_TIMER => { 544 | request_data[32] = constant::TS_RES_OCTET as u8 545 | } 546 | _ => request_data[32] = constant::TS_RES_BYTE as u8, // byte/word/dword etc. 547 | } 548 | // length 549 | BigEndian::write_u16(request_data[33..].as_mut(), length as u16); 550 | 551 | //expand values into array 552 | request_data.splice( 553 | 35..35, 554 | buffer[offset as usize..offset as usize + data_size as usize].to_vec(), 555 | ); 556 | 557 | let result = self.transport.send(request_data.as_mut_slice()); 558 | 559 | match result { 560 | Ok(response) => { 561 | if response.len() != 22 { 562 | return Err(Error::Response { 563 | code: error::ISO_INVALID_PDU, 564 | }); 565 | } 566 | 567 | if response[21] != 0xFF { 568 | return Err(Error::CPU { 569 | code: response[21] as i32, 570 | }); 571 | } 572 | } 573 | Err(e) => { 574 | return Err(e); 575 | } 576 | } 577 | 578 | offset += data_size; 579 | tot_elements -= num_elements; 580 | start += num_elements * word_size; 581 | } 582 | Ok(()) 583 | } 584 | } 585 | 586 | impl Client { 587 | /// Starting the CPU from power off,Current configuration is discarded and program processing begins again with the initial values. 588 | pub fn start(&mut self) -> Result<(), Error> { 589 | self.cold_warm_start_stop( 590 | transport::COLD_START_TELEGRAM.as_ref(), 591 | transport::PDU_START, 592 | error::CLI_CANNOT_START_PLC, 593 | transport::PDU_ALREADY_STARTED, 594 | error::CLI_ALREADY_RUN, 595 | ) 596 | } 597 | 598 | /// Restarting the CPU without turning the power off, Program processing starts once again where Retentive data is retained. 599 | pub fn restart(&mut self) -> Result<(), Error> { 600 | self.cold_warm_start_stop( 601 | transport::WARM_START_TELEGRAM.as_ref(), 602 | transport::PDU_START, 603 | error::CLI_CANNOT_START_PLC, 604 | transport::PDU_ALREADY_STARTED, 605 | error::CLI_ALREADY_RUN, 606 | ) 607 | } 608 | 609 | /// Shut down 610 | pub fn stop(&mut self) -> Result<(), Error> { 611 | self.cold_warm_start_stop( 612 | transport::STOP_TELEGRAM.as_ref(), 613 | transport::PDU_STOP, 614 | error::CLI_CANNOT_STOP_PLC, 615 | transport::PDU_ALREADY_STOPPED, 616 | error::CLI_ALREADY_STOP, 617 | ) 618 | } 619 | 620 | /// get plc status 621 | pub fn plc_status(&mut self) -> Result { 622 | let response = self 623 | .transport 624 | .send(transport::PLC_STATUS_TELEGRAM.as_ref())?; 625 | 626 | if response.len() < transport::PLC_STATUS_MIN_RESPONSE { 627 | return Err(Error::Response { 628 | code: error::ISO_INVALID_PDU, 629 | }); 630 | } 631 | 632 | let result = BigEndian::read_u16(response[27..29].as_ref()); 633 | 634 | if result != 0 { 635 | return Err(Error::CPU { 636 | code: result as i32, 637 | }); 638 | } 639 | 640 | CpuStatus::from_u8(response[44]) 641 | } 642 | 643 | pub fn cp_info(&mut self) -> Result { 644 | let szl = self.read_szl(0x0131, 0x000)?; 645 | 646 | Ok(CPInfo { 647 | max_pdu_length: BigEndian::read_u16(szl.data[2..].as_ref()), 648 | max_connections: BigEndian::read_u16(szl.data[4..].as_ref()), 649 | max_mpi_rate: BigEndian::read_u16(szl.data[6..].as_ref()), 650 | max_bus_rate: BigEndian::read_u16(szl.data[10..].as_ref()), 651 | }) 652 | } 653 | 654 | /// get cpu info 655 | pub fn cpu_info(&mut self) -> Result { 656 | let szl = self.read_szl(0x001C, 0x000)?; 657 | 658 | if szl.data.len() < transport::SZL_MIN_RESPONSE { 659 | return Err(Error::Response { 660 | code: error::ISO_INVALID_PDU, 661 | }); 662 | } 663 | 664 | let module_type_name = match str::from_utf8(szl.data[172..204].as_ref()) { 665 | Ok(s) => s, 666 | Err(e) => { 667 | return Err(Error::InvalidResponse { 668 | bytes: szl.data[172..204].to_vec(), 669 | reason: e.to_string(), 670 | }) 671 | } 672 | }; 673 | 674 | let serial_number = match str::from_utf8(szl.data[138..162].as_ref()) { 675 | Ok(s) => s, 676 | Err(e) => { 677 | return Err(Error::InvalidResponse { 678 | bytes: szl.data[138..162].to_vec(), 679 | reason: e.to_string(), 680 | }) 681 | } 682 | }; 683 | 684 | let as_name = match str::from_utf8(szl.data[2..26].as_ref()) { 685 | Ok(s) => s, 686 | Err(e) => { 687 | return Err(Error::InvalidResponse { 688 | bytes: szl.data[2..26].to_vec(), 689 | reason: e.to_string(), 690 | }) 691 | } 692 | }; 693 | 694 | let copyright = match str::from_utf8(szl.data[104..130].as_ref()) { 695 | Ok(s) => s, 696 | Err(e) => { 697 | return Err(Error::InvalidResponse { 698 | bytes: szl.data[104..130].to_vec(), 699 | reason: e.to_string(), 700 | }) 701 | } 702 | }; 703 | 704 | let module_name = match str::from_utf8(szl.data[36..60].as_ref()) { 705 | Ok(s) => s, 706 | Err(e) => { 707 | return Err(Error::InvalidResponse { 708 | bytes: szl.data[36..60].to_vec(), 709 | reason: e.to_string(), 710 | }) 711 | } 712 | }; 713 | 714 | Ok(CpuInfo { 715 | module_type_name: module_type_name.to_string(), 716 | serial_number: serial_number.to_string(), 717 | as_name: as_name.to_string(), 718 | copyright: copyright.to_string(), 719 | module_name: module_name.to_string(), 720 | }) 721 | } 722 | 723 | fn read_szl(&mut self, id: u16, index: u16) -> Result { 724 | let data_szl = 0; 725 | let mut offset = 0; 726 | let seq_out: u16 = 0x0000; 727 | 728 | let mut s7_szlfirst = transport::SZL_FIRST_TELEGRAM.to_vec(); 729 | 730 | BigEndian::write_u16(s7_szlfirst[11..].as_mut(), seq_out + 1); 731 | BigEndian::write_u16(s7_szlfirst[29..].as_mut(), id); 732 | BigEndian::write_u16(s7_szlfirst[31..].as_mut(), index); 733 | 734 | let mut res = self.transport.send(s7_szlfirst.as_ref())?; 735 | 736 | let validate = |res: &[u8], size: usize| -> Result<(), Error> { 737 | if res.len() < transport::MIN_SZL_FIRST_TELEGRAM + size { 738 | return Err(Error::Response { 739 | code: error::ISO_INVALID_PDU, 740 | }); 741 | } 742 | 743 | if BigEndian::read_u16(res[27..].as_ref()) != 0 && res[29] != 0xFF { 744 | return Err(Error::CPU { 745 | code: error::CLI_INVALID_PLC_ANSWER, 746 | }); 747 | } 748 | Ok(()) 749 | }; 750 | 751 | validate(res.as_ref(), 0)?; 752 | 753 | // Skips extra params (ID, Index ...) 754 | let mut data_szl = BigEndian::read_u16(res[31..].as_ref()) - 8; 755 | 756 | validate(res.as_ref(), data_szl as usize)?; 757 | 758 | let mut done = res[26] == 0x00; 759 | // Slice sequence 760 | let mut seq_in: u8 = res[24]; 761 | let header = transport::SZLHeader { 762 | length_header: BigEndian::read_u16(res[37..].as_ref()) * 2, 763 | number_of_data_record: BigEndian::read_u16(res[39..].as_ref()), 764 | }; 765 | 766 | let len = (offset + data_szl) as usize; 767 | let mut data = vec![0u8; len]; 768 | 769 | data[offset as usize..len].copy_from_slice(res[41..41 + data_szl as usize].as_ref()); 770 | 771 | let mut szl = transport::S7SZL { header, data }; 772 | offset += data_szl; 773 | 774 | let mut s7szlnext: Vec = transport::SZL_NEXT_TELEGRAM.to_vec(); 775 | 776 | while !done { 777 | BigEndian::write_u16(s7_szlfirst[11..].as_mut(), seq_out + 1); 778 | s7szlnext[24] = seq_in; 779 | 780 | res = self.transport.send(s7szlnext.as_ref())?; 781 | 782 | validate(res.as_ref(), 0)?; 783 | 784 | data_szl = BigEndian::read_u16(res[31..].as_ref()); 785 | done = res[26] == 0x00; 786 | seq_in = res[24]; 787 | 788 | szl.data = vec![0u8; len]; 789 | offset += data_szl; 790 | szl.header.length_header += szl.header.length_header; 791 | } 792 | Ok(szl) 793 | } 794 | 795 | fn cold_warm_start_stop( 796 | &mut self, 797 | req: &[u8], 798 | start_cmp: u8, 799 | start: i32, 800 | already_cmp: u8, 801 | already: i32, 802 | ) -> Result<(), Error> { 803 | let response = self.transport.send(req)?; 804 | 805 | if response.len() < transport::TELEGRAM_MIN_RESPONSE { 806 | return Err(Error::Response { 807 | code: error::ISO_INVALID_PDU, 808 | }); 809 | } 810 | 811 | if response[17] != start_cmp { 812 | return Err(Error::Response { code: start }); 813 | } 814 | if response[18] == already_cmp { 815 | return Err(Error::Response { code: already }); 816 | } 817 | Ok(()) 818 | } 819 | } 820 | -------------------------------------------------------------------------------- /src/constant.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | 3 | // Area ID 4 | #[derive(Clone, Copy)] 5 | #[allow(dead_code)] 6 | pub(crate) enum Area { 7 | ProcessInput = 0x81, 8 | ProcessOutput = 0x82, 9 | /// Merkers are address registers within the CPU. 10 | /// The number of available flag bytes depends on the respective CPU and can be taken from the technical data. 11 | /// You can use flag bits, flag bytes, flag words or flag double words in a PLC program. 12 | Merker = 0x83, 13 | /// German thing, means building blocks 14 | /// This is your storage 15 | DataBausteine = 0x84, 16 | Counter = 0x1C, 17 | Timer = 0x1D, 18 | Unknown, 19 | } 20 | 21 | // Word Length 22 | pub const WL_BIT: i32 = 0x01; //Bit (inside a word) 23 | pub const WL_BYTE: i32 = 0x02; //Byte (8 bit) 24 | pub const WL_CHAR: i32 = 0x03; 25 | pub const WL_WORD: i32 = 0x04; //Word (16 bit) 26 | pub const WL_INT: i32 = 0x05; 27 | pub const WL_DWORD: i32 = 0x06; //Double Word (32 bit) 28 | pub const WL_DINT: i32 = 0x07; 29 | pub const WL_REAL: i32 = 0x08; //Real (32 bit float) 30 | pub const WL_COUNTER: i32 = 0x1C; //Counter (16 bit) 31 | pub const WL_TIMER: i32 = 0x1D; //Timer (16 bit) 32 | 33 | //dataSize to number of byte accordingly 34 | pub fn data_size_byte(word_length: i32) -> i32 { 35 | match word_length { 36 | WL_BIT | WL_BYTE | WL_CHAR => 1, 37 | WL_WORD | WL_INT | WL_COUNTER | WL_TIMER => 2, 38 | WL_DWORD | WL_DINT | WL_REAL => 4, 39 | _ => 0, 40 | } 41 | } 42 | 43 | // PLC Status 44 | pub enum CpuStatus { 45 | Unknown = 0, 46 | Stop = 4, 47 | Run = 8, 48 | } 49 | 50 | impl CpuStatus { 51 | pub(crate) fn from_u8(value: u8) -> Result { 52 | match value { 53 | 0 => Ok(CpuStatus::Unknown), 54 | 4 => Ok(CpuStatus::Stop), 55 | 8 => Ok(CpuStatus::Run), 56 | _ => Err(Error::InvalidCpuStatus(value)), 57 | } 58 | } 59 | } 60 | 61 | //size header 62 | pub const SIZE_HEADER_READ: i32 = 31; // Header Size when Reading 63 | pub const SIZE_HEADER_WRITE: i32 = 35; // Header Size when Writing 64 | 65 | // Result transport size 66 | pub const TS_RES_BIT: i32 = 3; 67 | pub const TS_RES_BYTE: i32 = 4; 68 | #[allow(dead_code)] 69 | pub const TS_RES_INT: i32 = 5; 70 | //todo implement read write multi 71 | #[allow(dead_code)] 72 | pub const TS_RES_REAL: i32 = 7; 73 | pub const TS_RES_OCTET: i32 = 9; 74 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Petar Dambovaliev. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | use std::error; 6 | use std::fmt; 7 | use std::io::{Error as IOError, ErrorKind}; 8 | 9 | const TCP_SOCKET_CREATION: i32 = 1; 10 | const TCP_CONNECTION_TIMEOUT: i32 = 2; 11 | const TCP_CONNECTION_FAILED: i32 = 3; 12 | const TCP_RECEIVE_TIMEOUT: i32 = 4; 13 | const TCP_DATA_RECEIVE: i32 = -5; 14 | const TCP_SEND_TIMEOUT: i32 = 0x00000006; 15 | const TCP_DATA_SEND: i32 = 0x00000007; 16 | const TCP_CONNECTION_RESET: i32 = 0x00000008; 17 | const TCP_NOT_CONNECTED: i32 = 0x00000009; 18 | const TCP_UNREACHALE_HOST: i32 = 0x00002751; 19 | 20 | const ISO_CONNECT: i32 = 0x00010000; 21 | pub(crate) const ISO_INVALID_PDU: i32 = 0x00030000; // Bad format 22 | pub(crate) const ISO_INVALID_DATA_SIZE: i32 = 0x00040000; 23 | 24 | pub(crate) const CLI_NEGOTIATING_PDU: i32 = 0x00100000; 25 | const CLI_INVALID_PARAMS: i32 = 0x00200000; 26 | const CLI_JOB_PENDING: i32 = 0x00300000; 27 | const CLI_TOO_MANY_ITEMS: i32 = 0x00400000; 28 | const CLI_INVALID_DWORD_LEN: i32 = 0x00500000; 29 | const CLI_PARTIAL_DATA_WRITTEN: i32 = 0x00600000; 30 | const CLI_SIZE_OVER_PDU: i32 = 0x00700000; 31 | pub(crate) const CLI_INVALID_PLC_ANSWER: i32 = 0x00800000; 32 | const CLI_ADDRESS_OUT_OF_RANGE: i32 = 0x00900000; 33 | const CLI_INVALID_TRANSPORT_SIZE: i32 = 0x00A00000; 34 | const CLI_WRITE_DATA_SIZE_MISMATCH: i32 = 0x00B00000; 35 | const CLI_ITEM_NOT_AVAILABLE: i32 = 0x00C00000; 36 | const CLI_INVALID_VALUE: i32 = 0x00D00000; 37 | pub(crate) const CLI_CANNOT_START_PLC: i32 = 0x00E00000; 38 | pub(crate) const CLI_ALREADY_RUN: i32 = 0x00F00000; 39 | pub(crate) const CLI_CANNOT_STOP_PLC: i32 = 0x01000000; 40 | const CLI_CANNOT_COPY_RAM_TO_ROM: i32 = 0x01100000; 41 | const CLI_CANNOT_COMPRESS: i32 = 0x01200000; 42 | pub(crate) const CLI_ALREADY_STOP: i32 = 0x01300000; 43 | const CLI_FUN_NOT_AVAILABLE: i32 = 0x01400000; 44 | const CLI_UPLOAD_SEQUENCE_FAILED: i32 = 0x01500000; 45 | const CLI_INVALID_DATA_SIZE_RECVD: i32 = 0x01600000; 46 | const CLI_INVALID_BLOCK_TYPE: i32 = 0x01700000; 47 | const CLI_INVALID_BLOCK_NUMBER: i32 = 0x01800000; 48 | const CLI_INVALID_BLOCK_SIZE: i32 = 0x01900000; 49 | const CLI_NEED_PASSWORD: i32 = 0x01D00000; 50 | const CLI_INVALID_PASSWORD: i32 = 0x01E00000; 51 | const CLI_NO_PASSWORD_TO_SET_OR_CLEAR: i32 = 0x01F00000; 52 | const CLI_JOB_TIMEOUT: i32 = 0x02000000; 53 | const CLI_PARTIAL_DATA_READ: i32 = 0x02100000; 54 | const CLI_BUFFER_TOO_SMALL: i32 = 0x02200000; 55 | const CLI_FUNCTION_REFUSED: i32 = 0x02300000; 56 | const CLI_DESTROYING: i32 = 0x02400000; 57 | const CLI_INVALID_PARAM_NUMBER: i32 = 0x02500000; 58 | const CLI_CANNOT_CHANGE_PARAM: i32 = 0x02600000; 59 | const CLI_FUNCTION_NOT_IMPLEMENTED: i32 = 0x02700000; 60 | 61 | const CODE_7_ADDRESS_OUT_OF_RANGE: i32 = 5; 62 | const CODE_7_INVALID_TRANSPORT_SIZE: i32 = 6; 63 | const CODE_7_WRITE_DATA_SIZE_MISMATCH: i32 = 7; 64 | const CODE_7_RES_ITEM_NOT_AVAILABLE: i32 = 10; 65 | const CODE_7_RES_ITEM_NOT_AVAILABLE1: i32 = 53769; 66 | const CODE_7_INVALID_VALUE: i32 = 56321; 67 | const CODE_7_NEED_PASSWORD: i32 = 53825; 68 | const CODE_7_INVALID_PASSWORD: i32 = 54786; 69 | const CODE_7_NO_PASSWORD_TO_CLEAR: i32 = 54788; 70 | const CODE_7_NO_PASSWORD_TO_SET: i32 = 54789; 71 | const CODE_7_FUN_NOT_AVAILABLE: i32 = 33028; 72 | const CODE_7_DATA_OVER_PDU: i32 = 34048; 73 | 74 | #[derive(Debug)] 75 | pub enum Error { 76 | Connect(String), 77 | Lock, 78 | IOError(ErrorKind), 79 | Response { code: i32 }, 80 | CPU { code: i32 }, 81 | InvalidInput { input: String }, 82 | Send, 83 | Iso, 84 | PduLength(i32), 85 | TryFrom(Vec, String), 86 | InvalidCpuStatus(u8), 87 | InvalidResponse { reason: String, bytes: Vec }, 88 | } 89 | 90 | impl fmt::Display for Error { 91 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 92 | match self { 93 | Error::Connect(s) => write!(f, "connection error: {}", s), 94 | Error::Lock => write!(f, "Lock error: panicked"), 95 | Error::IOError(kind) => write!(f, "IO error: {:?}", kind), 96 | Error::Response { code } => write!(f, "Error response: {}", error_text(*code)), 97 | Error::CPU { code } => { 98 | write!(f, "Error response CPU: {}", error_text(cpu_error(*code))) 99 | } 100 | Error::InvalidInput { input } => write!(f, "Invalid input: {}", input), 101 | Error::Send => write!(f, "Send connection error"), 102 | Error::Iso => write!(f, "ISO connection error"), 103 | Error::PduLength(pdu) => write!(f, "PDU length connection error {}", pdu), 104 | Error::TryFrom(bytes, reason) => { 105 | write!(f, "Could not read bytes {:?} reason {}", bytes, reason) 106 | } 107 | Error::InvalidCpuStatus(status) => write!(f, "Invalid cpu status {}", status), 108 | Error::InvalidResponse { reason, bytes } => { 109 | write!(f, "Invalid response {:?} err {}", bytes, reason) 110 | } 111 | } 112 | } 113 | } 114 | 115 | impl From for Error { 116 | fn from(e: IOError) -> Self { 117 | Error::IOError(e.kind()) 118 | } 119 | } 120 | // This is important for other errors to wrap this one. 121 | impl error::Error for Error { 122 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 123 | None 124 | } 125 | } 126 | 127 | //CPUError specific CPU error after response 128 | fn cpu_error(err: i32) -> i32 { 129 | match err { 130 | CODE_7_ADDRESS_OUT_OF_RANGE => CLI_ADDRESS_OUT_OF_RANGE, 131 | CODE_7_INVALID_TRANSPORT_SIZE => CLI_INVALID_TRANSPORT_SIZE, 132 | CODE_7_WRITE_DATA_SIZE_MISMATCH => CLI_WRITE_DATA_SIZE_MISMATCH, 133 | CODE_7_RES_ITEM_NOT_AVAILABLE | CODE_7_RES_ITEM_NOT_AVAILABLE1 => CLI_ITEM_NOT_AVAILABLE, 134 | CODE_7_DATA_OVER_PDU => CLI_SIZE_OVER_PDU, 135 | CODE_7_INVALID_VALUE => CLI_INVALID_VALUE, 136 | CODE_7_FUN_NOT_AVAILABLE => CLI_FUN_NOT_AVAILABLE, 137 | CODE_7_NEED_PASSWORD => CLI_NEED_PASSWORD, 138 | CODE_7_INVALID_PASSWORD => CLI_INVALID_PASSWORD, 139 | CODE_7_NO_PASSWORD_TO_SET | CODE_7_NO_PASSWORD_TO_CLEAR => CLI_NO_PASSWORD_TO_SET_OR_CLEAR, 140 | _ => CLI_FUNCTION_REFUSED, 141 | } 142 | } 143 | 144 | //ErrorText return a string error text from error code integer 145 | fn error_text(err: i32) -> &'static str { 146 | match err { 147 | 0 => "OK", 148 | TCP_SOCKET_CREATION => "SYS : Error creating the Socket", 149 | TCP_CONNECTION_TIMEOUT => "TCP : Connection Timeout", 150 | TCP_CONNECTION_FAILED => "TCP : Connection Error", 151 | TCP_RECEIVE_TIMEOUT => "TCP : Data receive Timeout", 152 | TCP_DATA_RECEIVE => "TCP : Error receiving Data", 153 | TCP_SEND_TIMEOUT => "TCP : Data send Timeout", 154 | TCP_DATA_SEND => "TCP : Error sending Data", 155 | TCP_CONNECTION_RESET => "TCP : Connection reset by the Peer", 156 | TCP_NOT_CONNECTED => "CLI : Client not connected", 157 | TCP_UNREACHALE_HOST => "TCP : Unreachable host", 158 | 159 | ISO_CONNECT => "ISO : Connection Error", 160 | ISO_INVALID_PDU => "ISO : Invalid PDU received", 161 | ISO_INVALID_DATA_SIZE => "ISO : Invalid Buffer passed to Send/Receive", 162 | 163 | CLI_NEGOTIATING_PDU => "CLI : Error in PDU negotiation", 164 | CLI_INVALID_PARAMS => "CLI : invalid param(s) supplied", 165 | CLI_JOB_PENDING => "CLI : Job pending", 166 | CLI_TOO_MANY_ITEMS => "CLI : too may items (>20) in multi read/write", 167 | CLI_INVALID_DWORD_LEN => "CLI : invalid WordLength", 168 | CLI_PARTIAL_DATA_WRITTEN => "CLI : Partial data written", 169 | CLI_SIZE_OVER_PDU => "CPU : total data exceeds the PDU size", 170 | CLI_INVALID_PLC_ANSWER => "CLI : invalid CPU answer", 171 | CLI_ADDRESS_OUT_OF_RANGE => "CPU : Address out of range", 172 | CLI_INVALID_TRANSPORT_SIZE => "CPU : Invalid Transport size", 173 | CLI_WRITE_DATA_SIZE_MISMATCH => "CPU : Data size mismatch", 174 | CLI_ITEM_NOT_AVAILABLE => "CPU : Item not available", 175 | CLI_INVALID_VALUE => "CPU : Invalid value supplied", 176 | CLI_CANNOT_START_PLC => "CPU : Cannot start PLC", 177 | CLI_ALREADY_RUN => "CPU : PLC already RUN", 178 | CLI_CANNOT_STOP_PLC => "CPU : Cannot stop PLC", 179 | CLI_CANNOT_COPY_RAM_TO_ROM => "CPU : Cannot copy RAM to ROM", 180 | CLI_CANNOT_COMPRESS => "CPU : Cannot compress", 181 | CLI_ALREADY_STOP => "CPU : PLC already STOP", 182 | CLI_FUN_NOT_AVAILABLE => "CPU : Function not available", 183 | CLI_UPLOAD_SEQUENCE_FAILED => "CPU : Upload sequence failed", 184 | CLI_INVALID_DATA_SIZE_RECVD => "CLI : Invalid data size received", 185 | CLI_INVALID_BLOCK_TYPE => "CLI : Invalid block type", 186 | CLI_INVALID_BLOCK_NUMBER => "CLI : Invalid block number", 187 | CLI_INVALID_BLOCK_SIZE => "CLI : Invalid block size", 188 | CLI_NEED_PASSWORD => "CPU : Function not authorized for current protection level", 189 | CLI_INVALID_PASSWORD => "CPU : Invalid password", 190 | CLI_NO_PASSWORD_TO_SET_OR_CLEAR => "CPU : No password to set or clear", 191 | CLI_JOB_TIMEOUT => "CLI : Job Timeout", 192 | CLI_FUNCTION_REFUSED => "CLI : function refused by CPU (Unknown error)", 193 | CLI_PARTIAL_DATA_READ => "CLI : Partial data read", 194 | CLI_BUFFER_TOO_SMALL => { 195 | "CLI : The buffer supplied is too small to accomplish the operation" 196 | } 197 | CLI_DESTROYING => "CLI : Cannot perform (destroying)", 198 | CLI_INVALID_PARAM_NUMBER => "CLI : Invalid Param Number", 199 | CLI_CANNOT_CHANGE_PARAM => "CLI : Cannot change this param now", 200 | CLI_FUNCTION_NOT_IMPLEMENTED => "CLI : Function not implemented", 201 | _ => "CLI : Unknown error", 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/field.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Petar Dambovaliev. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | //! Parses bytes from `Area::DataBausteine` to types for easier manipulation 6 | 7 | use super::error::Error; 8 | use byteorder::{BigEndian, ByteOrder}; 9 | 10 | /// Fields collection type alias for convenience 11 | /// # Examples 12 | /// 13 | /// ``` 14 | /// use s7::field::{Float, Bool, Fields}; 15 | /// 16 | /// let float = Float::new(888, 8.0, vec![66, 86, 0, 0]).unwrap(); 17 | /// let boolean = Bool::new(888, 8.0, vec![1u8]).unwrap(); 18 | /// println!("bool: {}", boolean.value()); 19 | /// println!("float: {}", float.value()); 20 | /// let fields: Fields = vec![Box::new(float), Box::new(boolean)]; 21 | /// 22 | /// for field in fields.iter() { 23 | /// println!( 24 | /// "saving bytes {:?} to block {} offset {}", 25 | /// field.to_bytes(), 26 | /// field.data_block(), 27 | /// field.offset() 28 | /// ) 29 | /// } 30 | /// ``` 31 | pub type Fields = Vec>; 32 | 33 | /// represents a type stored in the hardware 34 | /// ie `bool`, `real(32 bit float)` 35 | pub trait Field { 36 | /// data block 37 | fn data_block(&self) -> i32; 38 | /// offset in the data block 39 | /// for convenience, we truncate the float 40 | /// we don't care about the digits after the decimal point anymore 41 | fn offset(&self) -> i32; 42 | 43 | fn to_bytes(&self) -> Vec; 44 | } 45 | 46 | /// PLC float field 47 | #[derive(Debug)] 48 | pub struct Float { 49 | data_block: i32, 50 | /// offset example 8.1 51 | /// left side is index within the block 52 | /// right side is the bit position only used for bool, zero for all other types 53 | offset: f32, 54 | value: f32, 55 | } 56 | 57 | impl Float { 58 | pub fn new(data_block: i32, offset: f32, mut bytes: Vec) -> Result { 59 | let len = bytes.len(); 60 | if bytes.len() != Float::size() as usize { 61 | return Err(Error::TryFrom( 62 | bytes, 63 | format!("Float.new: expected buf size {} got {}", Float::size(), len), 64 | )); 65 | } 66 | 67 | let bit_offset = ((offset * 10.0) as usize % 10) as u8; 68 | if bit_offset != 0 { 69 | return Err(Error::TryFrom( 70 | bytes, 71 | format!( 72 | "Float.new: float should not have a bit offset got {}", 73 | bit_offset 74 | ), 75 | )); 76 | } 77 | 78 | Ok(Float { 79 | data_block, 80 | offset, 81 | value: BigEndian::read_f32(bytes.as_mut_slice()), 82 | }) 83 | } 84 | 85 | pub fn size() -> i32 { 86 | 4 87 | } 88 | 89 | pub fn value(&self) -> f32 { 90 | self.value 91 | } 92 | 93 | pub fn set_value(&mut self, v: f32) { 94 | self.value = v 95 | } 96 | } 97 | 98 | impl Field for Float { 99 | fn data_block(&self) -> i32 { 100 | self.data_block 101 | } 102 | 103 | fn offset(&self) -> i32 { 104 | self.offset as i32 105 | } 106 | 107 | fn to_bytes(&self) -> Vec { 108 | let mut buf = vec![0u8; Float::size() as usize]; 109 | BigEndian::write_f32(buf.as_mut_slice(), self.value); 110 | return buf; 111 | } 112 | } 113 | 114 | #[derive(Debug)] 115 | pub struct Double { 116 | data_block: i32, 117 | /// offset example 8.1 118 | /// left side is index within the block 119 | /// right side is the bit position only used for bool, zero for all other types 120 | offset: f64, 121 | value: f64, 122 | } 123 | 124 | impl Double { 125 | pub fn new(data_block: i32, offset: f64, mut bytes: Vec) -> Result { 126 | let len = bytes.len(); 127 | if bytes.len() != Double::size() as usize { 128 | return Err(Error::TryFrom( 129 | bytes, 130 | format!("Double.new: expected buf size {} got {}", Double::size(), len), 131 | )); 132 | } 133 | 134 | let bit_offset = ((offset * 10.0) as usize % 10) as u8; 135 | if bit_offset != 0 { 136 | return Err(Error::TryFrom( 137 | bytes, 138 | format!( 139 | "Double.new: double should not have a bit offset got {}", 140 | bit_offset 141 | ), 142 | )); 143 | } 144 | 145 | Ok(Double { 146 | data_block, 147 | offset, 148 | value: BigEndian::read_f64(bytes.as_mut_slice()), 149 | }) 150 | } 151 | 152 | pub fn size() -> i32 { 153 | 8 154 | } 155 | 156 | pub fn value(&self) -> f64 { 157 | self.value 158 | } 159 | 160 | pub fn set_value(&mut self, v: f64) { 161 | self.value = v 162 | } 163 | } 164 | 165 | impl Field for Double { 166 | fn data_block(&self) -> i32 { 167 | self.data_block 168 | } 169 | 170 | fn offset(&self) -> i32 { 171 | self.offset as i32 172 | } 173 | 174 | fn to_bytes(&self) -> Vec { 175 | let mut buf = vec![0u8; Double::size() as usize]; 176 | BigEndian::write_f64(buf.as_mut_slice(), self.value); 177 | return buf; 178 | } 179 | } 180 | 181 | 182 | /// Bool represents a single bit in a byte from `Area::DataBausteine` 183 | #[derive(Debug)] 184 | pub struct Bool { 185 | /// index of the block it's stored at 186 | data_block: i32, 187 | /// example 8.1 188 | /// left side is index within the block 189 | /// right side is the bit position only used for bool, zero for all other types 190 | offset: f32, 191 | /// the actual primitive value 192 | byte: u8, 193 | /// the current value that will be written to the byte 194 | value: bool, 195 | } 196 | 197 | impl Bool { 198 | pub fn new(data_block: i32, offset: f32, bytes: Vec) -> Result { 199 | let len = bytes.len(); 200 | if bytes.len() != Self::size() as usize { 201 | return Err(Error::TryFrom( 202 | bytes, 203 | format!("Bool.new: expected buf size {} got {}", Self::size(), len), 204 | )); 205 | } 206 | 207 | let bit_offset = ((offset * 10.0) as usize % 10) as u8; 208 | if bit_offset > 7 { 209 | return Err(Error::TryFrom( 210 | bytes, 211 | format!("Bool.new: max offset is 7 got {}", offset), 212 | )); 213 | } 214 | 215 | Ok(Bool { 216 | data_block, 217 | offset, 218 | byte: bytes[0], 219 | value: bytes[0] & (1 << bit_offset) != 0, 220 | }) 221 | } 222 | 223 | #[inline(always)] 224 | fn set_value_at(b: u8, bit_pos: u8, val: bool) -> u8 { 225 | if val { 226 | return b | (1 << bit_pos); 227 | } 228 | return b & !(1 << bit_pos); 229 | } 230 | 231 | pub fn size() -> i32 { 232 | 1 233 | } 234 | 235 | /// gets the value at the current offset 236 | pub fn value(&self) -> bool { 237 | self.value 238 | } 239 | 240 | pub fn set_value(&mut self, v: bool) { 241 | self.value = v; 242 | self.byte = Bool::set_value_at( 243 | self.byte, 244 | ((self.offset * 10.0) as usize % 10) as u8, 245 | self.value, 246 | ); 247 | } 248 | } 249 | 250 | impl Field for Bool { 251 | fn data_block(&self) -> i32 { 252 | self.data_block 253 | } 254 | 255 | fn offset(&self) -> i32 { 256 | self.offset as i32 257 | } 258 | 259 | fn to_bytes(&self) -> Vec { 260 | vec![self.byte] 261 | } 262 | } 263 | 264 | /// PLC word field 265 | #[derive(Debug)] 266 | pub struct Word { 267 | data_block: i32, 268 | /// offset example 8.1 269 | /// left side is index within the block 270 | /// right side is the bit position only used for bool, zero for all other types 271 | offset: f32, 272 | value: u16, 273 | } 274 | 275 | impl Word { 276 | pub fn new(data_block: i32, offset: f32, mut bytes: Vec) -> Result { 277 | let len = bytes.len(); 278 | if bytes.len() != Word::size() as usize { 279 | return Err(Error::TryFrom( 280 | bytes, 281 | format!("Word.new: expected buf size {} got {}", Word::size(), len), 282 | )); 283 | } 284 | 285 | let bit_offset = ((offset * 10.0) as usize % 10) as u8; 286 | if bit_offset != 0 { 287 | return Err(Error::TryFrom( 288 | bytes, 289 | format!( 290 | "Word.new: float should not have a bit offset got {}", 291 | bit_offset 292 | ), 293 | )); 294 | } 295 | 296 | Ok(Word { 297 | data_block, 298 | offset, 299 | value: BigEndian::read_u16(bytes.as_mut_slice()), 300 | }) 301 | } 302 | 303 | pub fn size() -> i32 { 304 | 2 305 | } 306 | 307 | pub fn value(&self) -> u16 { 308 | self.value 309 | } 310 | 311 | pub fn set_value(&mut self, v: u16) { 312 | self.value = v 313 | } 314 | } 315 | 316 | impl Field for Word { 317 | fn data_block(&self) -> i32 { 318 | self.data_block 319 | } 320 | 321 | fn offset(&self) -> i32 { 322 | self.offset as i32 323 | } 324 | 325 | fn to_bytes(&self) -> Vec { 326 | let mut buf = vec![0u8; Word::size() as usize]; 327 | BigEndian::write_u16(buf.as_mut_slice(), self.value); 328 | return buf; 329 | } 330 | } 331 | 332 | 333 | #[test] 334 | fn test_fields() { 335 | let float = Float::new(888, 8.0, vec![66, 86, 0, 0]).unwrap(); 336 | let boolean = Bool::new(888, 8.0, vec![1u8]).unwrap(); 337 | assert!(boolean.value()); 338 | assert_eq!(53.5, float.value()); 339 | let fields: Fields = vec![Box::new(float), Box::new(boolean)]; 340 | 341 | for field in fields.iter() { 342 | println!( 343 | "saving bytes {:?} to block {} offset {}", 344 | field.to_bytes(), 345 | field.data_block(), 346 | field.offset() 347 | ) 348 | } 349 | } 350 | 351 | #[test] 352 | fn test_float() { 353 | let val: f32 = 53.5; 354 | let mut b = vec![0u8; Float::size() as usize]; 355 | BigEndian::write_f32(b.as_mut_slice(), val); 356 | let mut field = Float::new(888, 8.0, b).unwrap(); 357 | field.set_value(val); 358 | let result = field.to_bytes(); 359 | 360 | assert_eq!(vec![66, 86, 0, 0], result); 361 | 362 | // test invalid bit offset 363 | // float should not have a bit offset 364 | match Float::new(888, 8.1, vec![66, 86, 0, 0]) { 365 | Ok(_) => { 366 | println!("should return an error at invalid bit offset 1. Floats should not have a bit offset"); 367 | assert!(false) 368 | } 369 | Err(_) => {} 370 | } 371 | } 372 | 373 | #[test] 374 | fn test_bool() { 375 | let b = vec![1u8; 1]; 376 | let mut field = Bool::new(888, 8.1, b).unwrap(); 377 | field.set_value(true); 378 | 379 | let mut res: Vec = field.to_bytes(); 380 | 381 | assert_eq!(res.len(), 1); 382 | assert_eq!(res[0], 3); 383 | assert_eq!(field.value(), true); 384 | 385 | field.set_value(false); 386 | res = field.to_bytes(); 387 | 388 | assert_eq!(res.len(), 1); 389 | assert_eq!(res[0], 1); 390 | assert_eq!(field.value(), false); 391 | 392 | let bb = vec![0b00001000u8; 1]; 393 | field = Bool::new(888, 8.4, bb).unwrap(); 394 | field.set_value(true); 395 | 396 | res = field.to_bytes(); 397 | 398 | assert_eq!(res.len(), 1); 399 | assert_eq!(res[0], 24); 400 | assert_eq!(field.value(), true); 401 | 402 | // test invalid bit offset 403 | match Bool::new(888, 8.8, vec![0b00001000u8; 1]) { 404 | Ok(_) => { 405 | println!("should return an error at invalid bit offset 8"); 406 | assert!(false) 407 | } 408 | Err(_) => {} 409 | } 410 | } 411 | 412 | #[test] 413 | fn test_word() { 414 | let val: u16 = 43981; 415 | let mut b = vec![0u8; Word::size() as usize]; 416 | BigEndian::write_u16(b.as_mut_slice(), val); 417 | let mut field = Word::new(888, 8.0, b).unwrap(); 418 | field.set_value(val); 419 | let result = field.to_bytes(); 420 | 421 | assert_eq!(vec![171, 205], result); 422 | 423 | // test invalid bit offset 424 | // words should not have a bit offset 425 | match Word::new(888, 8.1, vec![12, 23]) { 426 | Ok(_) => { 427 | println!("should return an error at invalid bit offset 1. Words should not have a bit offset"); 428 | assert!(false) 429 | } 430 | Err(_) => {} 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Petar Dambovaliev. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | pub mod client; 6 | mod constant; 7 | pub mod error; 8 | pub mod field; 9 | pub mod tcp; 10 | pub mod transport; 11 | -------------------------------------------------------------------------------- /src/tcp.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Petar Dambovaliev. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | //! TCP transport implementation 6 | 7 | extern crate byteorder; 8 | 9 | use super::error::{self, Error}; 10 | use super::transport::{self, Transport as PackTrait}; 11 | use crate::transport::Connection; 12 | use byteorder::{BigEndian, ByteOrder}; 13 | use std::io::{Read, Write}; 14 | use std::net::{IpAddr, SocketAddrV4}; 15 | use std::net::TcpStream; 16 | use std::sync::Mutex; 17 | use std::time::Duration; 18 | 19 | /// Default TCP timeout 20 | pub const TIMEOUT: Duration = Duration::from_secs(10); 21 | /// Default TCP idle timeout 22 | pub const IDLE_TIMEOUT: Duration = Duration::from_secs(60); 23 | pub const MAX_LENGTH: usize = 2084; 24 | //messages 25 | const PDU_SIZE_REQUESTED: i32 = 480; 26 | pub const ISO_TCP: i32 = 102; //default isotcp port 27 | const ISO_HEADER_SIZE: i32 = 7; // TPKT+COTP Header Size 28 | const MIN_PDU_SIZE: i32 = 16; 29 | 30 | pub struct Transport { 31 | options: Options, 32 | stream: Mutex, 33 | } 34 | 35 | /// a set of options for the TCP connection 36 | #[derive(Debug, Clone)] 37 | pub struct Options { 38 | pub connection_timeout: Option, 39 | pub read_timeout: Duration, 40 | pub write_timeout: Duration, 41 | address: String, 42 | pub conn_type: transport::Connection, 43 | rack: u16, 44 | slot: u16, 45 | //Transport Service Access Point 46 | local_tsap: u16, 47 | remote_tsap: u16, 48 | local_tsap_high: u8, 49 | local_tsap_low: u8, 50 | remote_tsap_high: u8, 51 | remote_tsap_low: u8, 52 | last_pdu_type: u8, 53 | //PDULength variable to store pdu length after connect 54 | pdu_length: i32, 55 | } 56 | 57 | impl Options { 58 | pub fn new(address: IpAddr, port: i32, rack: u16, slot: u16, conn_type: Connection) -> Options { 59 | let port = match port { 60 | 0 => ISO_TCP, 61 | _ => port 62 | }; 63 | 64 | Options { 65 | connection_timeout: None, 66 | read_timeout: Duration::new(0, 0), 67 | write_timeout: Duration::new(0, 0), 68 | address: format!("{}:{}", address.to_string(), port.to_string()), //ip:102, 69 | conn_type, 70 | rack, 71 | slot, 72 | local_tsap: 0, 73 | remote_tsap: 0, 74 | local_tsap_high: 0, 75 | local_tsap_low: 0, 76 | remote_tsap_high: 0, 77 | remote_tsap_low: 0, 78 | last_pdu_type: 0, 79 | pdu_length: 0, 80 | } 81 | } 82 | } 83 | 84 | impl Transport { 85 | pub fn connect(options: Options) -> Result { 86 | let tcp_client = match options.connection_timeout { 87 | Some(timeout) => { 88 | // Trying connecting with timeout 89 | match options.address.parse::() { 90 | Ok(socket_address) => TcpStream::connect_timeout(&socket_address, timeout)?, 91 | Err(e) => return Err(Error::Connect(e.to_string())), 92 | } 93 | }, 94 | None => { 95 | // Trying connecting with no timeout defined 96 | TcpStream::connect(&options.address)? 97 | }, 98 | }; 99 | 100 | tcp_client.set_read_timeout(Some(options.read_timeout))?; 101 | tcp_client.set_write_timeout(Some(options.write_timeout))?; 102 | Ok(Transport { 103 | options, 104 | stream: Mutex::new(tcp_client), 105 | }) 106 | } 107 | 108 | fn set_tsap(&mut self) { 109 | let mut remote_tsap = ((self.connection_type() as u16) << 8) as u16 110 | + (self.options.rack * 0x20) 111 | + self.options.slot; 112 | let local_tsap: u16 = 0x0100 & 0x0000FFFF; 113 | remote_tsap = remote_tsap & 0x0000FFFF; 114 | 115 | self.options.local_tsap = local_tsap; 116 | self.options.local_tsap_high = (local_tsap >> 8) as u8; 117 | self.options.local_tsap_low = (local_tsap & 0x00FF) as u8; 118 | 119 | self.options.remote_tsap = remote_tsap; 120 | self.options.remote_tsap_high = (remote_tsap >> 8) as u8; 121 | self.options.remote_tsap_low = (remote_tsap as u8) & 0x00FF; 122 | } 123 | 124 | fn iso_connect(&mut self) -> Result<(), Error> { 125 | let mut msg = transport::ISO_CONNECTION_REQUEST_TELEGRAM.to_vec(); 126 | 127 | msg[16] = self.options.local_tsap_high; 128 | msg[17] = self.options.local_tsap_low; 129 | msg[20] = self.options.remote_tsap_high; 130 | msg[21] = self.options.remote_tsap_low; 131 | 132 | let r = self.send(msg.as_slice()); 133 | 134 | let n = match r { 135 | Ok(n) => n.len(), 136 | Err(e) => return Err(Error::Connect(e.to_string())), 137 | }; 138 | 139 | // Sends the connection request telegram 140 | if n != msg.len() { 141 | return Err(Error::PduLength(n as i32)); 142 | } 143 | 144 | if self.options.last_pdu_type != transport::CONFIRM_CONNECTION { 145 | return Err(Error::Iso); 146 | } 147 | Ok(()) 148 | } 149 | 150 | fn negotiate_pdu_length(&mut self) -> Result<(), Error> { 151 | // Set PDU Size Requested //lth 152 | let mut pdu_size_package = transport::PDU_NEGOTIATION_TELEGRAM.to_vec(); 153 | BigEndian::write_u16(pdu_size_package[23..].as_mut(), PDU_SIZE_REQUESTED as u16); 154 | 155 | // Sends the connection request telegram 156 | let response = self.send(pdu_size_package.as_slice())?; 157 | if response.len() == 27 && response[17] == 0 && response[18] == 0 { 158 | // 20 = size of Negotiate Answer 159 | // Get PDU Size Negotiated 160 | self.options.pdu_length = BigEndian::read_u16(&response[25..]) as i32; 161 | if self.options.pdu_length <= 0 { 162 | return Err(Error::Response { 163 | code: error::CLI_NEGOTIATING_PDU, 164 | }); 165 | } 166 | } else { 167 | return Err(Error::Response { 168 | code: error::CLI_NEGOTIATING_PDU, 169 | }); 170 | } 171 | Ok(()) 172 | } 173 | } 174 | 175 | impl PackTrait for Transport { 176 | fn send(&mut self, request: &[u8]) -> Result, Error> { 177 | // Send sends data to server and ensures response length is greater than header length. 178 | let mut stream = match self.stream.lock() { 179 | Ok(s) => s, 180 | Err(_) => return Err(Error::Lock), 181 | }; 182 | stream.write(request)?; 183 | 184 | let mut data = vec![0u8; MAX_LENGTH]; 185 | let mut length; 186 | 187 | loop { 188 | // Get TPKT (4 bytes) 189 | stream.read(&mut data[..4])?; 190 | 191 | // Read length, ignore transaction & protocol id (4 bytes) 192 | length = BigEndian::read_u16(&data[2..]); 193 | let length_n = length as i32; 194 | 195 | if length_n == ISO_HEADER_SIZE { 196 | stream.read(&mut data[4..7])?; 197 | } else { 198 | if length_n > PDU_SIZE_REQUESTED + ISO_HEADER_SIZE || length_n < MIN_PDU_SIZE { 199 | return Err(Error::PduLength(length_n)); 200 | } 201 | break; 202 | } 203 | } 204 | 205 | // Skip remaining 3 COTP bytes 206 | stream.read(&mut data[4..7])?; 207 | self.options.last_pdu_type = data[5]; // Stores PDU Type, we need it for later 208 | 209 | // Receives the S7 Payload 210 | stream.read(&mut data[7..length as usize])?; 211 | Ok(data[0..length as usize].to_vec()) 212 | } 213 | 214 | fn pdu_length(&self) -> i32 { 215 | self.options.pdu_length 216 | } 217 | 218 | fn negotiate(&mut self) -> Result<(), Error> { 219 | self.set_tsap(); 220 | self.iso_connect()?; 221 | self.negotiate_pdu_length() 222 | } 223 | 224 | fn connection_type(&self) -> Connection { 225 | self.options.conn_type 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/transport.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Petar Dambovaliev. All rights reserved. 2 | // This software may be modified and distributed under the terms 3 | // of the BSD license. See the LICENSE file for details. 4 | 5 | //! Transport definition for PLC 6 | 7 | use super::constant; 8 | use super::error::Error; 9 | 10 | /// Client Connection Type 11 | /// 16 possible connections limited by the hardware 12 | /// The types are defined from the highest to lowest priority 13 | /// The basic connections are the first which would be closed 14 | /// if there aren't enough resources 15 | #[derive(Debug, Copy, Clone)] 16 | pub enum Connection { 17 | /// Connect to the PLC programming console (Programmiergeräte). German for programming device. 18 | PG = 1, 19 | /// Connect to the PLC Siemens HMI panel 20 | OP = 2, 21 | /// Basic connection for generic data transfer connection 22 | /// 14 Basic connections 23 | Basic = 3, 24 | } 25 | 26 | /// an abstract communication used by the client to send requests 27 | /// ## How can I implement `Transport`? 28 | /// 29 | /// Types that are [`Transport`] should store the `pdu_length` 30 | /// at the connection phase `self.pdu_length = BigEndian::read_u16(&response[25..]) as i32;` 31 | pub trait Transport { 32 | /// send request to the plc. 33 | /// returns a response and an error, if there was any. 34 | fn send(&mut self, request: &[u8]) -> Result, Error>; 35 | /// pdu length needs to be set by the implementor, during the connection phase. 36 | fn pdu_length(&self) -> i32; 37 | /// negotiate is called by the client and should only be defined by the implementor 38 | fn negotiate(&mut self) -> Result<(), Error>; 39 | 40 | fn connection_type(&self) -> Connection; 41 | } 42 | 43 | /// response from the plc that the connection has been confirmed 44 | pub const CONFIRM_CONNECTION: u8 = 0xD0; 45 | 46 | /// ISO Connection Request telegram (contains also ISO Header and COTP Header) 47 | /// TPKT (RFC1006 Header) 48 | pub const ISO_CONNECTION_REQUEST_TELEGRAM: [u8; 22] = [ 49 | 3, // RFC 1006 ID (3) 50 | 0, // Reserved, always 0 51 | 0, // High part of packet lenght (entire frame, payload and TPDU included) 52 | 22, // Low part of packet lenght (entire frame, payload and TPDU included) 53 | // COTP (ISO 8073 Header) 54 | 17, // PDU Size Length 55 | 224, // CR - Connection Request ID 56 | 0, // Dst Reference HI 57 | 0, // Dst Reference LO 58 | 0, // Src Reference HI 59 | 1, // Src Reference LO 60 | 0, // Class + Options Flags 61 | 192, // PDU Max Length ID 62 | 1, // PDU Max Length HI 63 | 10, // PDU Max Length LO 64 | 193, // Src TSAP Identifier 65 | 2, // Src TSAP Length (2 bytes) 66 | 1, // Src TSAP HI (will be overwritten) 67 | 0, // Src TSAP LO (will be overwritten) 68 | 194, // Dst TSAP Identifier 69 | 2, // Dst TSAP Length (2 bytes) 70 | 1, // Dst TSAP HI (will be overwritten) 71 | 2, 72 | ]; // Dst TSAP LO (will be overwritten) 73 | 74 | /// S7 Read/Write Request Header (contains also ISO Header and COTP Header) 75 | pub const READ_WRITE_TELEGRAM: [u8; 35] = [ 76 | // 31-35 bytes 77 | 3, 78 | 0, 79 | 0, 80 | 31, // Telegram Length (Data Size + 31 or 35) 81 | 2, 82 | 240, 83 | 128, // COTP (see above for info) 84 | 50, // S7 Protocol ID 85 | 1, // Job Type 86 | 0, 87 | 0, // Redundancy identification 88 | 5, 89 | 0, // PDU Reference //lth this use for request S7 packet id 90 | 0, 91 | 14, // Parameters Length 92 | 0, 93 | 0, // Data Length = Size(bytes) + 4 94 | 4, // Function 4 Read Var, 5 Write Var 95 | 1, // Items count 96 | 18, // Var spec. 97 | 10, // Length of remaining bytes 98 | 16, // Syntax ID 99 | constant::WL_BYTE as u8, // Transport Size idx=22 100 | 0, 101 | 0, // Num Elements 102 | 0, 103 | 0, // DB Number (if any, else 0) 104 | 132, // Area Type 105 | 0, 106 | 0, 107 | 0, // Area Offset 108 | // WR area 109 | 0, // Reserved 110 | 4, // Transport size 111 | 0, 112 | 0, 113 | ]; // Data Length * 8 (if not bit or timer or counter) 114 | 115 | // used during establishing a connection 116 | pub const PDU_NEGOTIATION_TELEGRAM: [u8; 25] = [ 117 | 3, 0, 0, 25, 2, 240, 128, // TPKT + COTP (see above for info) 118 | 50, 1, 0, 0, 4, 0, 0, 8, 0, 0, 240, 0, 0, 1, 0, 1, 0, 30, 119 | ]; // PDU Length Requested = HI-LO Here Default 480 bytes 120 | 121 | /// warm start request 122 | pub(crate) const WARM_START_TELEGRAM: [u8; 37] = [ 123 | 3, 0, 0, 37, 2, 240, 128, 50, 1, 0, 0, 12, 0, 0, 20, 0, 0, 40, 0, 0, 0, 0, 0, 0, 253, 0, 0, 9, 124 | 80, 95, 80, 82, 79, 71, 82, 65, 77, 125 | ]; 126 | 127 | /// cold start request 128 | pub(crate) const COLD_START_TELEGRAM: [u8; 39] = [ 129 | 3, 0, 0, 39, 2, 240, 128, 50, 1, 0, 0, 15, 0, 0, 22, 0, 0, 40, 0, 0, 0, 0, 0, 0, 253, 0, 2, 67, 130 | 32, 9, 80, 95, 80, 82, 79, 71, 82, 65, 77, 131 | ]; 132 | 133 | /// stop request 134 | pub(crate) const STOP_TELEGRAM: [u8; 33] = [ 135 | 3, 0, 0, 33, 2, 240, 128, 50, 1, 0, 0, 14, 0, 0, 16, 0, 0, 41, 0, 0, 0, 0, 0, 9, 80, 95, 80, 136 | 82, 79, 71, 82, 65, 77, 137 | ]; 138 | 139 | /// get plc status telegram 140 | pub(crate) const PLC_STATUS_TELEGRAM: [u8; 33] = [ 141 | 3, 0, 0, 33, 2, 240, 128, 50, 7, 0, 0, 44, 0, 0, 8, 0, 8, 0, 1, 18, 4, 17, 68, 1, 0, 255, 9, 0, 142 | 4, 4, 36, 0, 0, 143 | ]; 144 | 145 | pub(crate) const SZL_FIRST_TELEGRAM: [u8; 33] = [ 146 | 3, 0, 0, 33, 2, 240, 128, 50, 7, 0, 0, 5, 0, // Sequence out 147 | 0, 8, 0, 8, 0, 1, 18, 4, 17, 68, 1, 0, 255, 9, 0, 4, 0, 0, // ID (29) 148 | 0, 0, 149 | ]; // Index (31)]; 150 | 151 | pub(crate) const MIN_SZL_FIRST_TELEGRAM: usize = 42; 152 | 153 | pub(crate) const SZL_NEXT_TELEGRAM: [u8; 33] = [ 154 | 3, 0, 0, 33, 2, 240, 128, 50, 7, 0, 0, 6, 0, 0, 12, 0, 4, 0, 1, 18, 8, 18, 68, 1, 155 | 1, // Sequence 156 | 0, 0, 0, 0, 10, 0, 0, 0, 157 | ]; // Index (31)]; 158 | 159 | pub(crate) const PLC_STATUS_MIN_RESPONSE: usize = 45; 160 | 161 | pub(crate) const TELEGRAM_MIN_RESPONSE: usize = 19; 162 | 163 | pub(crate) const SZL_MIN_RESPONSE: usize = 205; 164 | 165 | pub(crate) const PDU_START: u8 = 0x28; // CPU start 166 | pub(crate) const PDU_STOP: u8 = 0x29; // CPU stop 167 | 168 | pub(crate) const PDU_ALREADY_STARTED: u8 = 0x02; // CPU already in run mode 169 | pub(crate) const PDU_ALREADY_STOPPED: u8 = 0x07; // CPU already in stop mode 170 | 171 | pub(crate) struct SZLHeader { 172 | pub length_header: u16, 173 | pub number_of_data_record: u16, 174 | } 175 | 176 | pub(crate) struct S7SZL { 177 | pub header: SZLHeader, 178 | pub data: Vec, 179 | } 180 | --------------------------------------------------------------------------------