Skip to content

About

Rust port of snap7

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

snap7-client

A pure-Rust client for Siemens S7 PLCs.

This is a from-scratch reimplementation of the client half of Snap7 by Davide Nardella (github.com/davenardella/snap7). It speaks ISO-TCP (RFC 1006) and the S7 communication protocol directly, so it talks to S7-200/300/400/1200/1500 CPUs and compatible CPs with no C library, no unsafe, and no dependencies.

Server and partner roles are intentionally out of scope — this is client only.

[dependencies]
snap7-client = "0.1"

Relationship to the original Snap7

All credit for the protocol work belongs to the upstream project. Snap7 is the result of years of reverse-engineering the S7 protocol, and this crate would not exist without it.

This is not a binding, a wrapper, or a bindgen port. Nothing links against libsnap7. The C++ sources were used as the protocol specification, and the implementation was written fresh in idiomatic Rust — different architecture, different memory model, different error handling (see below).

The upstream sources are not vendored in this repository. If you want them side by side for comparison:

git clone https://github.com/davenardella/snap7.git

Because this is a derived work of an LGPL-3.0 project, this crate is licensed LGPL-3.0-or-later to match. See COPYING.LESSER.

Usage

use snap7::{Client, Area, data};

fn main() -> Result<(), snap7::Error> {
    // S7-300: rack 0, slot 2. S7-1200/1500: rack 0, slot 0 or 1.
    let mut plc = Client::connect_to("192.168.0.1", 0, 2)?;

    // Raw bytes
    let buf = plc.db_read_to_vec(1, 0, 16)?;

    // Typed access
    let setpoint = data::get_f32(&buf, 0)?;
    let running  = data::get_bit(&buf, 4, 0)?;
    println!("setpoint {setpoint}, running {running}");

    // Write a single bit — no read-modify-write cycle
    plc.write_bit(Area::DataBlock, 1, 4, 1, true)?;

    plc.disconnect();
    Ok(())
}

Several variables in one telegram (up to 20 items):

use snap7::{ReadItem, Area};

let items = [
    ReadItem::db_bytes(1, 0, 4),
    ReadItem::db_bytes(2, 10, 8),
    ReadItem::bytes(Area::Merker, 0, 2),
];

// The outer Result covers the exchange; each item reports its own outcome,
// because a CPU can reject items individually.
for (i, res) in plc.read_multi_vars(&items)?.into_iter().enumerate() {
    match res.data {
        Ok(bytes) => println!("item {i}: {bytes:02X?}"),
        Err(e)    => eprintln!("item {i} failed: {e}"),
    }
}

Two runnable examples are included:

cargo run --example browse -- 192.168.0.1 0 2   # identity, blocks, status
cargo run --example db_io  -- 192.168.0.1 1     # typed DB read/write

What is implemented

Area Functions
Connection connect_to, open, connect, disconnect, reconnect, TSAP/port/ref/PDU tuning, timeouts
Data I/O read_area, write_area, read_multi_vars, write_multi_vars, read_bit, write_bit
Shorthands db_read, db_write, mb_*, eb_*, ab_*, tm_*, ct_*
Directory list_blocks, list_blocks_of_type, block_info, block_info_from_image
Blocks upload, full_upload, download, delete_block, db_get, db_fill
System read_szl, read_szl_list, order_code, cpu_info, cp_info, plc_status, protection
Clock plc_date_time, set_plc_date_time, set_plc_date_time_now_utc
Control plc_hot_start, plc_cold_start, plc_stop, copy_ram_to_rom, compress
Security set_session_password, clear_session_password
Escape hatch iso_exchange_buffer for raw telegrams

The data module converts between PLC bytes and Rust values: BYTE/WORD/ DWORD/INT/DINT/REAL/LREAL/STRING/CHAR arrays, DATE_AND_TIME, DATE, TIME, TIME_OF_DAY, S5TIME, BCD counters and bits.

Not implemented

The async/job API (Cli_AsReadArea and friends). The C++ version spawns a thread per client to emulate it; in Rust you can wrap the blocking client in spawn_blocking, a worker thread, or a Mutex, which composes better than a bespoke callback API.

How it differs from the original

The wire protocol is byte-compatible. The surrounding design is not.

Architecture. Three independently testable layers — TCP transport, ISO-TCP (TPKT/COTP) framing, and the S7 client — instead of a five-deep C++ inheritance chain (TMsgSocket → TIsoTcpSocket → TSnap7Peer → TSnap7MicroClient → TSnap7Client) in which each level mutates shared state on its parent.

Memory safety. Snap7 casts raw pointers onto #pragma pack(1) structs and reads fields without checking the received length, so a truncated or hostile answer reads past the buffer. Every field access here is range checked and returns Error::InvalidPlcAnswer instead. The crate is #![forbid(unsafe_code)].

Allocation. Snap7 embeds a 64 KiB opData buffer plus a 4 KiB PDU in every client object (~70 KiB per connection, used or not). This crate reuses a single request buffer and allocates results on demand.

Ownership. Functions return Vec<u8>/String rather than writing into caller-supplied C arrays with int &Size in/out parameters.

Determinism. Block timestamps are decoded as UTC with an embedded civil calendar algorithm. Snap7 calls localtime, so the same PLC reports different dates on different machines.

Errors. A typed Error enum. The original numeric codes are preserved via Error::code() and the original strings via Error::text(), so tooling keyed on Snap7 error numbers keeps working.

Bugs found in the reference implementation

The packed struct offsets were re-derived independently and diffed against this port, which surfaced three defects upstream:

  1. Multi-PDU timer/counter reads. Snap7 advances the cursor with Start += NumElements * WordSize for every word length, but timers and counters are addressed by element index, not by byte. Any timer or counter read large enough to span two PDUs skips half the requested range.
  2. ListBlocksOfType overrun. The inner loop runs CThis + 1 times and then decrements the pointer, reading one item past the received data.
  3. Unvalidated answer lengths throughout the parsing code.

Testing

cargo test

68 tests run without any hardware. The integration suite (tests/mock_plc.rs) starts a mock CPU implementing the real wire protocol — COTP handshake, PDU negotiation, read/write jobs, user-data functions, multi-telegram SZL answers and COTP fragmentation — and asserts on the exact bytes the client puts on the wire, including PDU splitting, bit versus element addressing, odd-length item padding and CPU error mapping.

Hardware status: the data I/O, directory, SZL, clock, control and security paths are covered against the mock CPU. upload, full_upload and download are hand-verified against the C++ struct layouts but have not been exercised against a real CPU — treat them as unproven.

Compatibility notes

  • Default rack/slot is 0/2 (S7-300/400). Use 0/0 or 0/1 for S7-1200/1500.
  • On S7-1200/1500, "optimized block access" must be off for the DB you want to read, and PUT/GET communication must be enabled in the CPU protection settings.
  • The default connection type is PG. A CPU offers only one PG resource, so use ConnectionType::Basic (or Op) when TIA Portal is connected at the same time.

License

LGPL-3.0-or-later, matching the upstream Snap7 licensing. See COPYING.LESSER and COPYING.

Snap7 is Copyright © 2013–2025 Davide Nardella.

About

Rust port of snap7

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages