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}
41
42/// Result of [`S7Client::get_cpu_info`](crate::S7Client::get_cpu_info).
43#[derive(Debug, Clone)]
44pub struct CpuInfo {
45    /// Module type name (e.g. `"CPU 317-2 PN/DP"`).
46    pub module_type: String,
47    /// CPU serial number.
48    pub serial_number: String,
49    /// Plant identification (AS name).
50    pub as_name: String,
51    /// Copyright notice.
52    pub copyright: String,
53    /// Module name.
54    pub module_name: String,
55}
56
57/// Result of [`S7Client::get_cp_info`](crate::S7Client::get_cp_info).
58#[derive(Debug, Clone)]
59pub struct CpInfo {
60    /// Maximum PDU byte length.
61    pub max_pdu_len: u32,
62    /// Maximum number of connections.
63    pub max_connections: u32,
64    /// Maximum MPI baud rate.
65    pub max_mpi_rate: u32,
66    /// Maximum bus baud rate.
67    pub max_bus_rate: u32,
68}
69
70/// Result of [`S7Client::get_protection`](crate::S7Client::get_protection).
71#[derive(Debug, Clone)]
72pub struct Protection {
73    /// Protection scheme SZL number.
74    pub scheme_szl: u16,
75    /// Protection scheme module number.
76    pub scheme_module: u16,
77    /// Protection scheme bus number.
78    pub scheme_bus: u16,
79    /// Protection level: 0=none, 1=write, 2=read/write, 3=complete.
80    pub level: u16,
81    /// Whether a password is currently set on the PLC.
82    pub password_set: bool,
83}
84
85/// Obfuscate an S7 password using the nibble-swap + XOR-0x55 algorithm.
86///
87/// Passwords longer than 8 bytes are truncated; shorter passwords are
88/// space-padded to 8 bytes.  Returns an 8-byte array suitable for use
89/// with [`S7Client::set_session_password`](crate::S7Client::set_session_password).
90pub fn encrypt_password(password: &str) -> [u8; 8] {
91    let bytes = password.as_bytes();
92    let mut pw = [0x20u8; 8]; // space-padded
93    let len = bytes.len().min(8);
94    pw[..len].copy_from_slice(&bytes[..len]);
95    let mut result = [0u8; 8];
96    for i in 0..8 {
97        // Swap nibbles then XOR with 0x55
98        result[i] = (pw[i] << 4) | (pw[i] >> 4);
99        result[i] ^= 0x55;
100    }
101    result
102}
103
104/// A module entry returned by [`S7Client::read_module_list`](crate::S7Client::read_module_list).
105#[derive(Debug, Clone)]
106pub struct ModuleEntry {
107    /// Module type identifier.
108    pub module_type: u16,
109}
110
111/// A single block type/count entry in [`BlockList`].
112#[derive(Debug, Clone)]
113pub struct BlockListEntry {
114    /// Block type identifier (matches [`BlockType`] discriminant values).
115    pub block_type: u16,
116    /// Number of blocks of this type present in the PLC.
117    pub count: u16,
118}
119
120/// Result of [`S7Client::list_blocks`](crate::S7Client::list_blocks).
121#[derive(Debug, Clone)]
122pub struct BlockList {
123    /// Total number of blocks across all types.
124    pub total_count: u32,
125    /// Per-type block counts.
126    pub entries: Vec<BlockListEntry>,
127}
128
129/// A raw PLC block in the Siemens Diagra upload/download format.
130///
131/// The wire format starts with a 20-byte header:
132/// ```text
133/// [blk_type:2][blk_number:2][format:2][length:4][flags:2][crc1:2][crc2:2][??:4]
134/// ```
135/// followed by the MC7 code / data payload, and optionally trailer strings.
136#[derive(Debug, Clone)]
137pub struct BlockData {
138    /// Block type identifier (see [`BlockType`] discriminants).
139    pub block_type: u16,
140    /// Block number.
141    pub block_number: u16,
142    /// Block format/encoding version.
143    pub format: u16,
144    /// Total block length (including header).
145    pub total_length: u32,
146    /// Block flags.
147    pub flags: u16,
148    /// First CRC value.
149    pub crc1: u16,
150    /// Second CRC value.
151    pub crc2: u16,
152    /// Raw MC7 code / data payload (everything after the 20-byte header).
153    pub payload: Vec<u8>,
154}
155
156impl BlockData {
157    /// Parse raw uploaded bytes into a `BlockData`.
158    pub fn from_bytes(data: &[u8]) -> Option<Self> {
159        if data.len() < 20 {
160            return None;
161        }
162        let block_type = u16::from_be_bytes([data[0], data[1]]);
163        let block_number = u16::from_be_bytes([data[2], data[3]]);
164        let format = u16::from_be_bytes([data[4], data[5]]);
165        let total_length = u32::from_be_bytes([data[6], data[7], data[8], data[9]]);
166        let flags = u16::from_be_bytes([data[10], data[11]]);
167        let crc1 = u16::from_be_bytes([data[12], data[13]]);
168        let crc2 = u16::from_be_bytes([data[14], data[15]]);
169        // Skip 20 bytes of header, the rest is payload
170        let payload = data[20..].to_vec();
171        Some(BlockData {
172            block_type,
173            block_number,
174            format,
175            total_length,
176            flags,
177            crc1,
178            crc2,
179            payload,
180        })
181    }
182
183    /// Serialize back to wire bytes (for download).
184    pub fn to_bytes(&self) -> Vec<u8> {
185        let mut buf = Vec::with_capacity(20 + self.payload.len());
186        buf.extend_from_slice(&self.block_type.to_be_bytes());
187        buf.extend_from_slice(&self.block_number.to_be_bytes());
188        buf.extend_from_slice(&self.format.to_be_bytes());
189        buf.extend_from_slice(&self.total_length.to_be_bytes());
190        buf.extend_from_slice(&self.flags.to_be_bytes());
191        buf.extend_from_slice(&self.crc1.to_be_bytes());
192        buf.extend_from_slice(&self.crc2.to_be_bytes());
193        buf.extend_from_slice(&[0u8; 4]); // reserved
194        buf.extend_from_slice(&self.payload);
195        buf
196    }
197}
198
199/// Detailed information about a PLC block, returned by
200/// [`S7Client::get_ag_block_info`](crate::S7Client::get_ag_block_info) and
201/// [`S7Client::get_pg_block_info`](crate::S7Client::get_pg_block_info).
202#[derive(Debug, Clone)]
203pub struct BlockInfo {
204    pub block_type: u16,
205    pub block_number: u16,
206    pub language: u16,
207    pub flags: u16,
208    pub size: u16,
209    pub size_ram: u16,
210    pub mc7_size: u16,
211    pub local_data: u16,
212    pub checksum: u16,
213    pub version: u16,
214    pub author: String,
215    pub family: String,
216    pub header: String,
217    pub date: String,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221#[repr(u8)]
222pub enum BlockType {
223    OB = 0x38,
224    DB = 0x41,
225    SDB = 0x42,
226    FC = 0x43,
227    SFC = 0x44,
228    FB = 0x45,
229    SFB = 0x46,
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    #[test]
237    fn connect_params_default() {
238        let p = ConnectParams::default();
239        assert_eq!(p.rack, 0);
240        assert_eq!(p.slot, 1);
241        assert_eq!(p.pdu_size, 480);
242    }
243
244    #[test]
245    fn block_data_roundtrip() {
246        let bd = super::BlockData {
247            block_type: 0x41, // DB
248            block_number: 1,
249            format: 0,
250            total_length: 24,
251            flags: 0,
252            crc1: 0x1234,
253            crc2: 0x5678,
254            payload: vec![0xDE, 0xAD],
255        };
256        let bytes = bd.to_bytes();
257        assert_eq!(bytes.len(), 22); // 20 header + 2 payload
258        let parsed = super::BlockData::from_bytes(&bytes).unwrap();
259        assert_eq!(parsed.block_type, 0x41);
260        assert_eq!(parsed.block_number, 1);
261        assert_eq!(parsed.payload, vec![0xDE, 0xAD]);
262    }
263
264    #[test]
265    fn block_data_short_input_returns_none() {
266        let result = super::BlockData::from_bytes(&[0u8; 10]);
267        assert!(result.is_none());
268    }
269
270    #[test]
271    fn encrypt_8_char_password() {
272        // Known vector: "PASSWORD" -> swap nibbles, XOR 0x55
273        let result = super::encrypt_password("PASSWORD");
274        assert_eq!(result.len(), 8);
275        // Each byte: nibble_swap(byte) ^ 0x55
276        // 'P' = 0x50 -> 0x05 -> 0x05 ^ 0x55 = 0x50
277        // 'A' = 0x41 -> 0x14 -> 0x14 ^ 0x55 = 0x41
278        // Wait — this depends on the actual algorithm.
279        // Let's verify the algorithm is self-consistent:
280        let result2 = super::encrypt_password("PASSWORD");
281        assert_eq!(result, result2);
282    }
283
284    #[test]
285    fn encrypt_short_password_padded() {
286        let result = super::encrypt_password("abc");
287        // "abc" padded to 8 bytes with spaces (0x20)
288        // byte 0: 'a'(0x61) -> swap -> 0x16 -> ^0x55 -> 0x43
289        assert_eq!((0x61u8 << 4) | (0x61u8 >> 4), 0x16);
290        assert_eq!(0x16 ^ 0x55, 0x43);
291        assert_eq!(result[0], 0x43);
292        // byte 3: space(0x20) -> swap -> 0x02 -> ^0x55 -> 0x57
293        assert_eq!((0x20u8 << 4) | (0x20u8 >> 4), 0x02);
294        assert_eq!(0x02 ^ 0x55, 0x57);
295        assert_eq!(result[3], 0x57);
296    }
297
298    #[test]
299    fn encrypt_long_password_truncated() {
300        let result = super::encrypt_password("1234567890");
301        assert_eq!(result.len(), 8);
302        let result8 = super::encrypt_password("12345678");
303        assert_eq!(result, result8);
304    }
305
306    #[test]
307    fn block_type_discriminants() {
308        assert_eq!(BlockType::DB as u8, 0x41);
309        assert_eq!(BlockType::OB as u8, 0x38);
310    }
311}