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
185impl BlockData {
186    /// Parse raw uploaded bytes into a `BlockData`.
187    pub fn from_bytes(data: &[u8]) -> Option<Self> {
188        if data.len() < 20 {
189            return None;
190        }
191        let block_type = u16::from_be_bytes([data[0], data[1]]);
192        let block_number = u16::from_be_bytes([data[2], data[3]]);
193        let format = u16::from_be_bytes([data[4], data[5]]);
194        let total_length = u32::from_be_bytes([data[6], data[7], data[8], data[9]]);
195        let flags = u16::from_be_bytes([data[10], data[11]]);
196        let crc1 = u16::from_be_bytes([data[12], data[13]]);
197        let crc2 = u16::from_be_bytes([data[14], data[15]]);
198        // Skip 20 bytes of header, the rest is payload
199        let payload = data[20..].to_vec();
200        Some(BlockData {
201            block_type,
202            block_number,
203            format,
204            total_length,
205            flags,
206            crc1,
207            crc2,
208            payload,
209        })
210    }
211
212    /// Serialize back to wire bytes (for download).
213    pub fn to_bytes(&self) -> Vec<u8> {
214        let mut buf = Vec::with_capacity(20 + self.payload.len());
215        buf.extend_from_slice(&self.block_type.to_be_bytes());
216        buf.extend_from_slice(&self.block_number.to_be_bytes());
217        buf.extend_from_slice(&self.format.to_be_bytes());
218        buf.extend_from_slice(&self.total_length.to_be_bytes());
219        buf.extend_from_slice(&self.flags.to_be_bytes());
220        buf.extend_from_slice(&self.crc1.to_be_bytes());
221        buf.extend_from_slice(&self.crc2.to_be_bytes());
222        buf.extend_from_slice(&[0u8; 4]); // reserved
223        buf.extend_from_slice(&self.payload);
224        buf
225    }
226}
227
228/// Detailed information about a PLC block, returned by
229/// [`S7Client::get_ag_block_info`](crate::S7Client::get_ag_block_info) and
230/// [`S7Client::get_pg_block_info`](crate::S7Client::get_pg_block_info).
231#[derive(Debug, Clone)]
232pub struct BlockInfo {
233    pub block_type: u16,
234    pub block_number: u16,
235    pub language: u16,
236    pub flags: u16,
237    pub size: u16,
238    pub size_ram: u16,
239    pub mc7_size: u16,
240    pub local_data: u16,
241    pub checksum: u16,
242    pub version: u16,
243    pub author: String,
244    pub family: String,
245    pub header: String,
246    pub date: String,
247}
248
249#[derive(Debug, Clone, Copy, PartialEq, Eq)]
250#[repr(u8)]
251pub enum BlockType {
252    OB = 0x38,
253    DB = 0x41,
254    SDB = 0x42,
255    FC = 0x43,
256    SFC = 0x44,
257    FB = 0x45,
258    SFB = 0x46,
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn connect_params_default() {
267        let p = ConnectParams::default();
268        assert_eq!(p.rack, 0);
269        assert_eq!(p.slot, 1);
270        assert_eq!(p.pdu_size, 480);
271    }
272
273    #[test]
274    fn block_data_roundtrip() {
275        let bd = super::BlockData {
276            block_type: 0x41, // DB
277            block_number: 1,
278            format: 0,
279            total_length: 24,
280            flags: 0,
281            crc1: 0x1234,
282            crc2: 0x5678,
283            payload: vec![0xDE, 0xAD],
284        };
285        let bytes = bd.to_bytes();
286        assert_eq!(bytes.len(), 22); // 20 header + 2 payload
287        let parsed = super::BlockData::from_bytes(&bytes).unwrap();
288        assert_eq!(parsed.block_type, 0x41);
289        assert_eq!(parsed.block_number, 1);
290        assert_eq!(parsed.payload, vec![0xDE, 0xAD]);
291    }
292
293    #[test]
294    fn block_data_short_input_returns_none() {
295        let result = super::BlockData::from_bytes(&[0u8; 10]);
296        assert!(result.is_none());
297    }
298
299    #[test]
300    fn encrypt_8_char_password() {
301        // Known vector: "PASSWORD" -> swap nibbles, XOR 0x55
302        let result = super::encrypt_password("PASSWORD");
303        assert_eq!(result.len(), 8);
304        // Each byte: nibble_swap(byte) ^ 0x55
305        // 'P' = 0x50 -> 0x05 -> 0x05 ^ 0x55 = 0x50
306        // 'A' = 0x41 -> 0x14 -> 0x14 ^ 0x55 = 0x41
307        // Wait — this depends on the actual algorithm.
308        // Let's verify the algorithm is self-consistent:
309        let result2 = super::encrypt_password("PASSWORD");
310        assert_eq!(result, result2);
311    }
312
313    #[test]
314    fn encrypt_short_password_padded() {
315        let result = super::encrypt_password("abc");
316        // "abc" padded to 8 bytes with spaces (0x20)
317        // byte 0: 'a'(0x61) -> swap -> 0x16 -> ^0x55 -> 0x43
318        assert_eq!((0x61u8 << 4) | (0x61u8 >> 4), 0x16);
319        assert_eq!(0x16 ^ 0x55, 0x43);
320        assert_eq!(result[0], 0x43);
321        // byte 3: space(0x20) -> swap -> 0x02 -> ^0x55 -> 0x57
322        assert_eq!((0x20u8 << 4) | (0x20u8 >> 4), 0x02);
323        assert_eq!(0x02 ^ 0x55, 0x57);
324        assert_eq!(result[3], 0x57);
325    }
326
327    #[test]
328    fn encrypt_long_password_truncated() {
329        let result = super::encrypt_password("1234567890");
330        assert_eq!(result.len(), 8);
331        let result8 = super::encrypt_password("12345678");
332        assert_eq!(result, result8);
333    }
334
335    #[test]
336    fn block_type_discriminants() {
337        assert_eq!(BlockType::DB as u8, 0x41);
338        assert_eq!(BlockType::OB as u8, 0x38);
339    }
340}