Skip to main content

snap7_client/
types.rs

1use std::time::Duration;
2
3#[derive(Debug, Clone)]
4pub struct ConnectParams {
5    pub rack: u8,
6    pub slot: u8,
7    pub pdu_size: u16,
8    pub connect_timeout: Duration,
9    pub request_timeout: Duration,
10}
11
12impl Default for ConnectParams {
13    fn default() -> Self {
14        Self {
15            rack: 0,
16            slot: 1,
17            pdu_size: 480,
18            connect_timeout: Duration::from_secs(5),
19            request_timeout: Duration::from_secs(10),
20        }
21    }
22}
23
24/// PLC run-time status returned by [`S7Client::get_plc_status`](crate::S7Client::get_plc_status).
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PlcStatus {
27    /// Status unknown or not available.
28    Unknown = 0x00,
29    /// PLC is in STOP mode.
30    Stop = 0x04,
31    /// PLC is in RUN mode.
32    Run = 0x08,
33}
34
35/// Result of [`S7Client::get_order_code`](crate::S7Client::get_order_code).
36#[derive(Debug, Clone)]
37pub struct OrderCode {
38    /// The order number (e.g. `"6ES7 317-2EK14-0AB0"`).
39    pub code: String,
40    /// Firmware version major component.
41    pub v1: u8,
42    /// Firmware version minor component.
43    pub v2: u8,
44    /// Firmware version patch component.
45    pub v3: u8,
46}
47
48/// Protocol variant used by the PLC.
49///
50/// - **S7** — Classic S7 protocol, used by S7-300, S7-400, S7-1200
51/// - **S7Plus** — S7+ (S7-Plus) protocol, used by S7-1500
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum Protocol {
54    /// Classic S7 protocol (S7-300, S7-400, S7-1200).
55    S7,
56    /// S7+ protocol (S7-1500).
57    S7Plus,
58}
59
60impl std::fmt::Display for Protocol {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Protocol::S7 => write!(f, "S7"),
64            Protocol::S7Plus => write!(f, "S7+"),
65        }
66    }
67}
68
69/// Result of [`S7Client::get_cpu_info`](crate::S7Client::get_cpu_info).
70#[derive(Debug, Clone)]
71pub struct CpuInfo {
72    /// Module type name (e.g. `"CPU 317-2 PN/DP"`).
73    pub module_type: String,
74    /// CPU serial number.
75    pub serial_number: String,
76    /// Plant identification (AS name).
77    pub as_name: String,
78    /// Copyright notice.
79    pub copyright: String,
80    /// Module name.
81    pub module_name: String,
82    /// Protocol version used by the PLC (S7 for 300/400, S7+ for 1500).
83    pub protocol: Protocol,
84}
85
86/// Result of [`S7Client::get_cp_info`](crate::S7Client::get_cp_info).
87#[derive(Debug, Clone)]
88pub struct CpInfo {
89    /// Maximum PDU byte length.
90    pub max_pdu_len: u32,
91    /// Maximum number of connections.
92    pub max_connections: u32,
93    /// Maximum MPI baud rate.
94    pub max_mpi_rate: u32,
95    /// Maximum bus baud rate.
96    pub max_bus_rate: u32,
97}
98
99/// Result of [`S7Client::get_protection`](crate::S7Client::get_protection).
100#[derive(Debug, Clone)]
101pub struct Protection {
102    /// Protection scheme SZL number.
103    pub scheme_szl: u16,
104    /// Protection scheme module number.
105    pub scheme_module: u16,
106    /// Protection scheme bus number.
107    pub scheme_bus: u16,
108    /// Protection level: 0=none, 1=write, 2=read/write, 3=complete.
109    pub level: u16,
110    /// Whether a password is currently set on the PLC.
111    pub password_set: bool,
112}
113
114/// Obfuscate an S7 password using the nibble-swap + XOR-0x55 algorithm.
115///
116/// Passwords longer than 8 bytes are truncated; shorter passwords are
117/// space-padded to 8 bytes.  Returns an 8-byte array suitable for use
118/// with [`S7Client::set_session_password`](crate::S7Client::set_session_password).
119pub fn encrypt_password(password: &str) -> [u8; 8] {
120    let bytes = password.as_bytes();
121    let mut pw = [0x20u8; 8]; // space-padded
122    let len = bytes.len().min(8);
123    pw[..len].copy_from_slice(&bytes[..len]);
124    let mut result = [0u8; 8];
125    for i in 0..8 {
126        // Swap nibbles then XOR with 0x55
127        result[i] = (pw[i] << 4) | (pw[i] >> 4);
128        result[i] ^= 0x55;
129    }
130    result
131}
132
133/// A module entry returned by [`S7Client::read_module_list`](crate::S7Client::read_module_list).
134#[derive(Debug, Clone)]
135pub struct ModuleEntry {
136    /// Module type identifier.
137    pub module_type: u16,
138}
139
140/// A single block type/count entry in [`BlockList`].
141#[derive(Debug, Clone)]
142pub struct BlockListEntry {
143    /// Block type identifier (matches [`BlockType`] discriminant values).
144    pub block_type: u16,
145    /// Number of blocks of this type present in the PLC.
146    pub count: u16,
147}
148
149/// Result of [`S7Client::list_blocks`](crate::S7Client::list_blocks).
150#[derive(Debug, Clone)]
151pub struct BlockList {
152    /// Total number of blocks across all types.
153    pub total_count: u32,
154    /// Per-type block counts.
155    pub entries: Vec<BlockListEntry>,
156}
157
158/// A raw PLC block in the Siemens Diagra upload/download format.
159///
160/// The wire format starts with a 20-byte header:
161/// ```text
162/// [blk_type:2][blk_number:2][format:2][length:4][flags:2][crc1:2][crc2:2][??:4]
163/// ```
164/// followed by the MC7 code / data payload, and optionally trailer strings.
165#[derive(Debug, Clone)]
166pub struct BlockData {
167    /// Block type identifier (see [`BlockType`] discriminants).
168    pub block_type: u16,
169    /// Block number.
170    pub block_number: u16,
171    /// Block format/encoding version.
172    pub format: u16,
173    /// Total block length (including header).
174    pub total_length: u32,
175    /// Block flags.
176    pub flags: u16,
177    /// First CRC value.
178    pub crc1: u16,
179    /// Second CRC value.
180    pub crc2: u16,
181    /// Raw MC7 code / data payload (everything after the 20-byte header).
182    pub payload: Vec<u8>,
183}
184
185/// Attributes that can be set on a block header.
186#[derive(Debug, Clone, Default)]
187pub struct BlockAttributes {
188    /// Author string (max 8 chars, padded with spaces).
189    pub author: Option<String>,
190    /// Family string (max 8 chars, padded with spaces).
191    pub family: Option<String>,
192    /// Header/name string (max 8 chars, padded with spaces).
193    pub name: Option<String>,
194    /// Version (major.minor encoded as `(major << 4) | minor`).
195    pub version: Option<u8>,
196    /// Block flags (overrides existing flags word).
197    pub flags: Option<u16>,
198}
199
200impl BlockData {
201    /// Parse raw uploaded bytes into a `BlockData`.
202    pub fn from_bytes(data: &[u8]) -> Option<Self> {
203        if data.len() < 20 {
204            return None;
205        }
206        let block_type = u16::from_be_bytes([data[0], data[1]]);
207        let block_number = u16::from_be_bytes([data[2], data[3]]);
208        let format = u16::from_be_bytes([data[4], data[5]]);
209        let total_length = u32::from_be_bytes([data[6], data[7], data[8], data[9]]);
210        let flags = u16::from_be_bytes([data[10], data[11]]);
211        let crc1 = u16::from_be_bytes([data[12], data[13]]);
212        let crc2 = u16::from_be_bytes([data[14], data[15]]);
213        // Skip 20 bytes of header, the rest is payload
214        let payload = data[20..].to_vec();
215        Some(BlockData {
216            block_type,
217            block_number,
218            format,
219            total_length,
220            flags,
221            crc1,
222            crc2,
223            payload,
224        })
225    }
226
227    /// Serialize back to wire bytes (for download).
228    pub fn to_bytes(&self) -> Vec<u8> {
229        let mut buf = Vec::with_capacity(20 + self.payload.len());
230        buf.extend_from_slice(&self.block_type.to_be_bytes());
231        buf.extend_from_slice(&self.block_number.to_be_bytes());
232        buf.extend_from_slice(&self.format.to_be_bytes());
233        buf.extend_from_slice(&self.total_length.to_be_bytes());
234        buf.extend_from_slice(&self.flags.to_be_bytes());
235        buf.extend_from_slice(&self.crc1.to_be_bytes());
236        buf.extend_from_slice(&self.crc2.to_be_bytes());
237        buf.extend_from_slice(&[0u8; 4]); // reserved
238        buf.extend_from_slice(&self.payload);
239        buf
240    }
241
242    /// Build a minimal empty DB block ready for download.
243    ///
244    /// Creates a Diagra-format block with the S7 DB header structure.
245    /// `size_bytes` is the desired DB size in bytes (must be even).
246    pub fn new_db(db_number: u16, size_bytes: u16) -> Self {
247        // Minimal S7 DB block payload: 2-byte "actual size" + zero data
248        let size = (size_bytes as usize + 1) & !1; // round up to even
249        let mut payload = Vec::with_capacity(2 + size);
250        payload.extend_from_slice(&(size as u16).to_be_bytes());
251        payload.extend(std::iter::repeat(0u8).take(size));
252        let total_length = (20 + payload.len()) as u32;
253        BlockData {
254            block_type: BlockType::DB as u16,
255            block_number: db_number,
256            format: 0x0001,
257            total_length,
258            flags: 0x0000,
259            crc1: 0x0000,
260            crc2: 0x0000,
261            payload,
262        }
263    }
264
265    /// Compute a CRC-32 checksum of the serialized block bytes.
266    ///
267    /// Suitable for comparing a locally stored block against one uploaded
268    /// from the PLC: `local.crc32() == plc_block.crc32()`.
269    pub fn crc32(&self) -> u32 {
270        let bytes = self.to_bytes();
271        crc32_ieee(&bytes)
272    }
273
274    /// Apply [`BlockAttributes`] to this block in-place.
275    ///
276    /// The S7 block footer is at `payload[payload.len()-48..]` (when payload
277    /// is large enough).  Author/Family/Name each occupy 8 bytes at fixed
278    /// offsets within the footer.
279    pub fn set_attributes(&mut self, attrs: &BlockAttributes) {
280        if let Some(f) = attrs.flags {
281            self.flags = f;
282        }
283        // Footer is last 48 bytes of payload (S7 block structure)
284        let plen = self.payload.len();
285        if plen < 48 {
286            return;
287        }
288        let footer = &mut self.payload[plen - 48..];
289        // Footer layout (S7 standard):
290        //   [0..8]   reserved
291        //   [8..16]  author (8 bytes, space-padded)
292        //   [16..24] family (8 bytes, space-padded)
293        //   [24..32] name/header (8 bytes, space-padded)
294        //   [32]     version byte
295        //   [33..48] reserved/checksum
296        if let Some(ref s) = attrs.author {
297            write_padded(&mut footer[8..16], s);
298        }
299        if let Some(ref s) = attrs.family {
300            write_padded(&mut footer[16..24], s);
301        }
302        if let Some(ref s) = attrs.name {
303            write_padded(&mut footer[24..32], s);
304        }
305        if let Some(v) = attrs.version {
306            footer[32] = v;
307        }
308    }
309
310    /// Return the human-readable block type name.
311    pub fn type_name(&self) -> &'static str {
312        block_type_name(self.block_type as u8)
313    }
314}
315
316pub fn block_type_name(bt: u8) -> &'static str {
317    match bt {
318        0x38 => "OB",
319        0x41 => "DB",
320        0x42 => "SDB",
321        0x43 => "FC",
322        0x44 => "SFC",
323        0x45 => "FB",
324        0x46 => "SFB",
325        0x47 => "UDT",
326        _ => "??",
327    }
328}
329
330fn write_padded(dst: &mut [u8], s: &str) {
331    let bytes = s.as_bytes();
332    let n = bytes.len().min(dst.len());
333    dst[..n].copy_from_slice(&bytes[..n]);
334    for b in dst[n..].iter_mut() {
335        *b = b' ';
336    }
337}
338
339// CRC-32 (IEEE 802.3 polynomial 0xEDB88320) — no external dep needed.
340fn crc32_ieee(data: &[u8]) -> u32 {
341    let mut crc: u32 = 0xFFFF_FFFF;
342    for &byte in data {
343        crc ^= byte as u32;
344        for _ in 0..8 {
345            if crc & 1 != 0 {
346                crc = (crc >> 1) ^ 0xEDB8_8320;
347            } else {
348                crc >>= 1;
349            }
350        }
351    }
352    !crc
353}
354
355/// Compare two block lists: local files vs PLC blocks.
356///
357/// Each entry is `(block_type, block_number)` in `local`; the closure
358/// `plc_crc` is called for each to retrieve the PLC-side CRC.
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub enum BlockCmpResult {
361    /// Identical CRC — block matches.
362    Match,
363    /// CRC differs — block has been modified on the PLC.
364    Mismatch { local_crc: u32, plc_crc: u32 },
365    /// Block exists locally but not on the PLC.
366    OnlyLocal,
367    /// Block exists on the PLC but not locally.
368    OnlyPlc,
369}
370
371/// Detailed information about a PLC block, returned by
372/// [`S7Client::get_ag_block_info`](crate::S7Client::get_ag_block_info) and
373/// [`S7Client::get_pg_block_info`](crate::S7Client::get_pg_block_info).
374#[derive(Debug, Clone)]
375pub struct BlockInfo {
376    pub block_type: u16,
377    pub block_number: u16,
378    pub language: u16,
379    pub flags: u16,
380    pub size: u16,
381    pub size_ram: u16,
382    pub mc7_size: u16,
383    pub local_data: u16,
384    pub checksum: u16,
385    pub version: u16,
386    pub author: String,
387    pub family: String,
388    pub header: String,
389    pub date: String,
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
393#[repr(u8)]
394pub enum BlockType {
395    OB = 0x38,
396    DB = 0x41,
397    SDB = 0x42,
398    FC = 0x43,
399    SFC = 0x44,
400    FB = 0x45,
401    SFB = 0x46,
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407
408    #[test]
409    fn connect_params_default() {
410        let p = ConnectParams::default();
411        assert_eq!(p.rack, 0);
412        assert_eq!(p.slot, 1);
413        assert_eq!(p.pdu_size, 480);
414    }
415
416    #[test]
417    fn block_data_roundtrip() {
418        let bd = super::BlockData {
419            block_type: 0x41, // DB
420            block_number: 1,
421            format: 0,
422            total_length: 24,
423            flags: 0,
424            crc1: 0x1234,
425            crc2: 0x5678,
426            payload: vec![0xDE, 0xAD],
427        };
428        let bytes = bd.to_bytes();
429        assert_eq!(bytes.len(), 22); // 20 header + 2 payload
430        let parsed = super::BlockData::from_bytes(&bytes).unwrap();
431        assert_eq!(parsed.block_type, 0x41);
432        assert_eq!(parsed.block_number, 1);
433        assert_eq!(parsed.payload, vec![0xDE, 0xAD]);
434    }
435
436    #[test]
437    fn block_data_short_input_returns_none() {
438        let result = super::BlockData::from_bytes(&[0u8; 10]);
439        assert!(result.is_none());
440    }
441
442    #[test]
443    fn encrypt_8_char_password() {
444        // Known vector: "PASSWORD" -> swap nibbles, XOR 0x55
445        let result = super::encrypt_password("PASSWORD");
446        assert_eq!(result.len(), 8);
447        // Each byte: nibble_swap(byte) ^ 0x55
448        // 'P' = 0x50 -> 0x05 -> 0x05 ^ 0x55 = 0x50
449        // 'A' = 0x41 -> 0x14 -> 0x14 ^ 0x55 = 0x41
450        // Wait — this depends on the actual algorithm.
451        // Let's verify the algorithm is self-consistent:
452        let result2 = super::encrypt_password("PASSWORD");
453        assert_eq!(result, result2);
454    }
455
456    #[test]
457    fn encrypt_short_password_padded() {
458        let result = super::encrypt_password("abc");
459        // "abc" padded to 8 bytes with spaces (0x20)
460        // byte 0: 'a'(0x61) -> swap -> 0x16 -> ^0x55 -> 0x43
461        assert_eq!((0x61u8 << 4) | (0x61u8 >> 4), 0x16);
462        assert_eq!(0x16 ^ 0x55, 0x43);
463        assert_eq!(result[0], 0x43);
464        // byte 3: space(0x20) -> swap -> 0x02 -> ^0x55 -> 0x57
465        assert_eq!((0x20u8 << 4) | (0x20u8 >> 4), 0x02);
466        assert_eq!(0x02 ^ 0x55, 0x57);
467        assert_eq!(result[3], 0x57);
468    }
469
470    #[test]
471    fn encrypt_long_password_truncated() {
472        let result = super::encrypt_password("1234567890");
473        assert_eq!(result.len(), 8);
474        let result8 = super::encrypt_password("12345678");
475        assert_eq!(result, result8);
476    }
477
478    #[test]
479    fn block_type_discriminants() {
480        assert_eq!(BlockType::DB as u8, 0x41);
481        assert_eq!(BlockType::OB as u8, 0x38);
482    }
483}