Skip to main content

snap7_client/
client.rs

1use bytes::{Buf, BufMut, Bytes, BytesMut};
2use std::net::SocketAddr;
3use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
4use tokio::sync::Mutex;
5
6use crate::proto::{
7    cotp::CotpPdu,
8    s7::{
9        clock::PlcDateTime,
10        header::{Area, PduType, S7Header, TransportSize},
11        read_var::{AddressItem, ReadVarRequest, ReadVarResponse},
12        szl::{SzlRequest, SzlResponse},
13        write_var::{WriteItem, WriteVarRequest, WriteVarResponse},
14    },
15    tpkt::TpktFrame,
16};
17
18use crate::{
19    connection::{connect, Connection},
20    error::{Error, Result},
21    types::ConnectParams,
22};
23
24/// A single item in a `read_multi_vars` request.
25#[derive(Debug, Clone)]
26pub struct MultiReadItem {
27    pub area: Area,
28    pub db_number: u16,
29    pub start: u32,
30    pub length: u16,
31    pub transport: TransportSize,
32}
33
34impl MultiReadItem {
35    /// Convenience constructor for a DataBlock byte read.
36    pub fn db(db: u16, start: u32, length: u16) -> Self {
37        Self {
38            area: Area::DataBlock,
39            db_number: db,
40            start,
41            length,
42            transport: TransportSize::Byte,
43        }
44    }
45}
46
47/// A single item in a `write_multi_vars` request.
48#[derive(Debug, Clone)]
49pub struct MultiWriteItem {
50    pub area: Area,
51    pub db_number: u16,
52    pub start: u32,
53    pub data: Bytes,
54}
55
56impl MultiWriteItem {
57    /// Convenience constructor for a DataBlock byte write.
58    pub fn db(db: u16, start: u32, data: impl Into<Bytes>) -> Self {
59        Self {
60            area: Area::DataBlock,
61            db_number: db,
62            start,
63            data: data.into(),
64        }
65    }
66}
67
68struct Inner<T> {
69    transport: T,
70    connection: Connection,
71    pdu_ref: u16,
72    request_timeout: std::time::Duration,
73}
74
75pub struct S7Client<T: AsyncRead + AsyncWrite + Unpin + Send> {
76    inner: Mutex<Inner<T>>,
77    params: ConnectParams,
78}
79
80impl<T: AsyncRead + AsyncWrite + Unpin + Send> S7Client<T> {
81    pub async fn from_transport(transport: T, params: ConnectParams) -> Result<Self> {
82        let mut t = transport;
83        let connection = connect(&mut t, &params).await?;
84        let timeout = params.request_timeout;
85        Ok(S7Client {
86            inner: Mutex::new(Inner {
87                transport: t,
88                connection,
89                pdu_ref: 1,
90                request_timeout: timeout,
91            }),
92            params,
93        })
94    }
95
96    /// Return the current request timeout.
97    pub fn request_timeout(&self) -> std::time::Duration {
98        self.params.request_timeout
99    }
100
101    /// Update the request timeout at runtime.
102    ///
103    /// This affects subsequent `recv_s7` calls made by this client instance.
104    pub async fn set_request_timeout(&self, timeout: std::time::Duration) {
105        let mut inner = self.inner.lock().await;
106        inner.request_timeout = timeout;
107    }
108
109    /// Read a client parameter by name.
110    ///
111    /// Supported names: `"request_timeout"`, `"connect_timeout"`, `"pdu_size"`.
112    pub fn get_param(&self, name: &str) -> Result<std::time::Duration> {
113        match name {
114            "request_timeout" => Ok(self.params.request_timeout),
115            "connect_timeout" => Ok(self.params.connect_timeout),
116            "pdu_size" => Err(Error::PlcError {
117                code: 0,
118                message: "pdu_size is not a Duration; use .params.pdu_size directly".into(),
119            }),
120            _ => Err(Error::PlcError {
121                code: 0,
122                message: format!("unknown parameter: {name}"),
123            }),
124        }
125    }
126
127    /// Set a client parameter at runtime.
128    ///
129    /// Supported names: `"request_timeout"` (Duration).
130    pub fn set_param(&mut self, name: &str, value: std::time::Duration) -> Result<()> {
131        match name {
132            "request_timeout" => {
133                self.params.request_timeout = value;
134                Ok(())
135            }
136            _ => Err(Error::PlcError {
137                code: 0,
138                message: format!("unknown parameter: {name}"),
139            }),
140        }
141    }
142
143    fn next_pdu_ref(inner: &mut Inner<T>) -> u16 {
144        inner.pdu_ref = inner.pdu_ref.wrapping_add(1);
145        inner.pdu_ref
146    }
147
148    async fn send_s7(
149        inner: &mut Inner<T>,
150        param_buf: Bytes,
151        data_buf: Bytes,
152        pdu_ref: u16,
153        pdu_type: PduType,
154    ) -> Result<()> {
155        let header = S7Header {
156            pdu_type,
157            reserved: 0,
158            pdu_ref,
159            param_len: param_buf.len() as u16,
160            data_len: data_buf.len() as u16,
161            error_class: None,
162            error_code: None,
163        };
164        let mut s7b = BytesMut::new();
165        header.encode(&mut s7b);
166        s7b.extend_from_slice(&param_buf);
167        s7b.extend_from_slice(&data_buf);
168
169        let dt = CotpPdu::Data {
170            tpdu_nr: 0,
171            last: true,
172            payload: s7b.freeze(),
173        };
174        let mut cotpb = BytesMut::new();
175        dt.encode(&mut cotpb);
176        let tpkt = TpktFrame {
177            payload: cotpb.freeze(),
178        };
179        let mut tb = BytesMut::new();
180        tpkt.encode(&mut tb)?;
181        inner.transport.write_all(&tb).await?;
182        Ok(())
183    }
184
185    async fn recv_s7(inner: &mut Inner<T>) -> Result<(S7Header, Bytes)> {
186        let timeout = inner.request_timeout;
187        let mut tpkt_hdr = [0u8; 4];
188        tokio::time::timeout(timeout, inner.transport.read_exact(&mut tpkt_hdr))
189            .await
190            .map_err(|_| Error::Timeout(timeout))??;
191        let total = u16::from_be_bytes([tpkt_hdr[2], tpkt_hdr[3]]) as usize;
192        if total < 4 {
193            return Err(Error::UnexpectedResponse);
194        }
195        let mut payload = vec![0u8; total - 4];
196        tokio::time::timeout(timeout, inner.transport.read_exact(&mut payload))
197            .await
198            .map_err(|_| Error::Timeout(timeout))??;
199        let mut b = Bytes::from(payload);
200
201        // COTP DT header: LI (1) + code (1) + tpdu_nr (1)
202        if b.remaining() < 3 {
203            return Err(Error::UnexpectedResponse);
204        }
205        let _li = b.get_u8();
206        let cotp_code = b.get_u8();
207        if cotp_code != 0xF0 {
208            return Err(Error::UnexpectedResponse);
209        }
210        b.advance(1); // tpdu_nr byte
211
212        let header = S7Header::decode(&mut b)?;
213        Ok((header, b))
214    }
215
216    pub async fn db_read(&self, db: u16, start: u32, length: u16) -> Result<Bytes> {
217        let mut inner = self.inner.lock().await;
218        let pdu_ref = Self::next_pdu_ref(&mut inner);
219
220        let req = ReadVarRequest {
221            items: vec![AddressItem {
222                area: Area::DataBlock,
223                db_number: db,
224                start,
225                bit_offset: 0,
226                length,
227                transport: TransportSize::Byte,
228            }],
229        };
230        let mut param_buf = BytesMut::new();
231        req.encode(&mut param_buf);
232
233        Self::send_s7(
234            &mut inner,
235            param_buf.freeze(),
236            Bytes::new(),
237            pdu_ref,
238            PduType::Job,
239        )
240        .await?;
241
242        let (header, mut body) = Self::recv_s7(&mut inner).await?;
243        check_plc_error(&header, "db_read")?;
244        if body.remaining() >= 2 {
245            body.advance(2); // skip param echo: func + item count
246        }
247        let resp = ReadVarResponse::decode(&mut body, 1)?;
248        if resp.items.is_empty() {
249            return Err(Error::UnexpectedResponse);
250        }
251        if resp.items[0].return_code != 0xFF {
252            return Err(Error::PlcError {
253                code: resp.items[0].return_code as u32,
254                message: "item error".into(),
255            });
256        }
257        Ok(resp.items[0].data.clone())
258    }
259
260    /// Read multiple PLC regions in one or more S7 PDU exchanges.
261    ///
262    /// Automatically batches items when the item count would exceed the Siemens hard
263    /// limit of 20 per PDU, or when the encoded request or response would exceed the
264    /// negotiated PDU size. Returns one `Bytes` per item in input order.
265    ///
266    /// Unlike `db_read`, this accepts any `Area` and `TransportSize`.
267    pub async fn read_multi_vars(&self, items: &[MultiReadItem]) -> Result<Vec<Bytes>> {
268        if items.is_empty() {
269            return Ok(Vec::new());
270        }
271
272        // PDU size constants (in bytes)
273        // S7 header: 10, func+count: 2, per-item address: 12
274        const S7_HEADER: usize = 10;
275        const PARAM_OVERHEAD: usize = 2; // func + item count
276        const ADDR_ITEM_SIZE: usize = 12;
277        // Response data item: 4 header + data + 0/1 pad
278        const DATA_ITEM_OVERHEAD: usize = 4;
279        const MAX_ITEMS_PER_PDU: usize = 20;
280
281        let mut inner = self.inner.lock().await;
282        let pdu_size = inner.connection.pdu_size as usize;
283        let max_req_payload = pdu_size.saturating_sub(S7_HEADER + PARAM_OVERHEAD);
284        let max_resp_payload = pdu_size.saturating_sub(S7_HEADER + PARAM_OVERHEAD);
285
286        let mut results = vec![Bytes::new(); items.len()];
287        let mut batch_start = 0;
288
289        while batch_start < items.len() {
290            // Build a batch that fits within PDU limits
291            let mut batch_end = batch_start;
292            let mut req_bytes_used = 0usize;
293            let mut resp_bytes_used = 0usize;
294
295            while batch_end < items.len() && (batch_end - batch_start) < MAX_ITEMS_PER_PDU {
296                let item = &items[batch_end];
297                let item_resp_size =
298                    DATA_ITEM_OVERHEAD + item.length as usize + (item.length as usize % 2);
299
300                if batch_end > batch_start
301                    && (req_bytes_used + ADDR_ITEM_SIZE > max_req_payload
302                        || resp_bytes_used + item_resp_size > max_resp_payload)
303                {
304                    break;
305                }
306                req_bytes_used += ADDR_ITEM_SIZE;
307                resp_bytes_used += item_resp_size;
308                batch_end += 1;
309            }
310
311            let batch = &items[batch_start..batch_end];
312            let pdu_ref = Self::next_pdu_ref(&mut inner);
313
314            let req = ReadVarRequest {
315                items: batch
316                    .iter()
317                    .map(|item| AddressItem {
318                        area: item.area,
319                        db_number: item.db_number,
320                        start: item.start,
321                        bit_offset: 0,
322                        // Siemens requires Byte transport + byte-count length in the request.
323                        // The item's declared transport is only used to decode the response.
324                        length: item.length,
325                        transport: TransportSize::Byte,
326                    })
327                    .collect(),
328            };
329            let mut param_buf = BytesMut::new();
330            req.encode(&mut param_buf);
331
332            Self::send_s7(
333                &mut inner,
334                param_buf.freeze(),
335                Bytes::new(),
336                pdu_ref,
337                PduType::Job,
338            )
339            .await?;
340
341            let (header, mut body) = Self::recv_s7(&mut inner).await?;
342            check_plc_error(&header, "read_multi_vars")?;
343            if body.remaining() >= 2 {
344                body.advance(2); // skip func + item_count echo
345            }
346            let resp = ReadVarResponse::decode(&mut body, batch.len())?;
347
348            for (i, item) in resp.items.into_iter().enumerate() {
349                if item.return_code != 0xFF {
350                    return Err(Error::PlcError {
351                        code: item.return_code as u32,
352                        message: format!("item {} error", batch_start + i),
353                    });
354                }
355                results[batch_start + i] = item.data;
356            }
357
358            batch_start = batch_end;
359        }
360
361        Ok(results)
362    }
363
364    /// Write multiple PLC regions in one or more S7 PDU exchanges.
365    ///
366    /// Automatically batches items when the count or encoded size would exceed the
367    /// negotiated PDU size or the Siemens hard limit of 20 items per PDU.
368    /// Returns `Ok(())` only when all items are acknowledged with return code 0xFF.
369    pub async fn write_multi_vars(&self, items: &[MultiWriteItem]) -> Result<()> {
370        if items.is_empty() {
371            return Ok(());
372        }
373
374        const S7_HEADER: usize = 10;
375        const PARAM_OVERHEAD: usize = 2; // func + item count
376        const ADDR_ITEM_SIZE: usize = 12;
377        const DATA_ITEM_OVERHEAD: usize = 4; // reserved + transport + bit_len (2)
378        const MAX_ITEMS_PER_PDU: usize = 20;
379
380        let mut inner = self.inner.lock().await;
381        let pdu_size = inner.connection.pdu_size as usize;
382        let max_payload = pdu_size.saturating_sub(S7_HEADER + PARAM_OVERHEAD);
383
384        let mut batch_start = 0;
385
386        while batch_start < items.len() {
387            let mut batch_end = batch_start;
388            let mut bytes_used = 0usize;
389
390            while batch_end < items.len() && (batch_end - batch_start) < MAX_ITEMS_PER_PDU {
391                let item = &items[batch_end];
392                let data_len = item.data.len();
393                let item_size = ADDR_ITEM_SIZE + DATA_ITEM_OVERHEAD + data_len + (data_len % 2);
394
395                if batch_end > batch_start && bytes_used + item_size > max_payload {
396                    break;
397                }
398                bytes_used += item_size;
399                batch_end += 1;
400            }
401
402            let batch = &items[batch_start..batch_end];
403            let pdu_ref = Self::next_pdu_ref(&mut inner);
404
405            let req = WriteVarRequest {
406                items: batch
407                    .iter()
408                    .map(|item| WriteItem {
409                        address: AddressItem {
410                            area: item.area,
411                            db_number: item.db_number,
412                            start: item.start,
413                            bit_offset: 0,
414                            length: item.data.len() as u16,
415                            transport: TransportSize::Byte,
416                        },
417                        data: item.data.clone(),
418                    })
419                    .collect(),
420            };
421            let mut param_buf = BytesMut::new();
422            req.encode(&mut param_buf);
423
424            Self::send_s7(
425                &mut inner,
426                param_buf.freeze(),
427                Bytes::new(),
428                pdu_ref,
429                PduType::Job,
430            )
431            .await?;
432
433            let (header, mut body) = Self::recv_s7(&mut inner).await?;
434            check_plc_error(&header, "write_multi_vars")?;
435            if body.remaining() >= 2 {
436                body.advance(2); // skip func + item_count echo
437            }
438            let resp = WriteVarResponse::decode(&mut body, batch.len())?;
439            for (i, &code) in resp.return_codes.iter().enumerate() {
440                if code != 0xFF {
441                    return Err(Error::PlcError {
442                        code: code as u32,
443                        message: format!("item {} write error", batch_start + i),
444                    });
445                }
446            }
447
448            batch_start = batch_end;
449        }
450
451        Ok(())
452    }
453
454    pub async fn db_write(&self, db: u16, start: u32, data: &[u8]) -> Result<()> {
455        let mut inner = self.inner.lock().await;
456        let pdu_ref = Self::next_pdu_ref(&mut inner);
457
458        let req = WriteVarRequest {
459            items: vec![WriteItem {
460                address: AddressItem {
461                    area: Area::DataBlock,
462                    db_number: db,
463                    start,
464                    bit_offset: 0,
465                    length: data.len() as u16,
466                    transport: TransportSize::Byte,
467                },
468                data: Bytes::copy_from_slice(data),
469            }],
470        };
471        let mut param_buf = BytesMut::new();
472        req.encode(&mut param_buf);
473
474        Self::send_s7(
475            &mut inner,
476            param_buf.freeze(),
477            Bytes::new(),
478            pdu_ref,
479            PduType::Job,
480        )
481        .await?;
482
483        let (header, mut body) = Self::recv_s7(&mut inner).await?;
484        check_plc_error(&header, "db_write")?;
485        if body.has_remaining() {
486            body.advance(2); // skip func + item count
487        }
488        let resp = WriteVarResponse::decode(&mut body, 1)?;
489        if resp.return_codes[0] != 0xFF {
490            return Err(Error::PlcError {
491                code: resp.return_codes[0] as u32,
492                message: "write error".into(),
493            });
494        }
495        Ok(())
496    }
497
498    /// Read from any PLC area using absolute addressing.
499    ///
500    /// A convenience wrapper around [`read_multi_vars`](Self::read_multi_vars)
501    /// for a single area read.
502    pub async fn ab_read(
503        &self,
504        area: Area,
505        db_number: u16,
506        start: u32,
507        length: u16,
508    ) -> Result<Bytes> {
509        let items = [MultiReadItem {
510            area,
511            db_number,
512            start,
513            length,
514            transport: TransportSize::Byte,
515        }];
516        let mut results = self.read_multi_vars(&items).await?;
517        Ok(results.swap_remove(0))
518    }
519
520    /// Write to any PLC area using absolute addressing.
521    ///
522    /// A convenience wrapper around [`write_multi_vars`](Self::write_multi_vars)
523    /// for a single area write.
524    pub async fn ab_write(
525        &self,
526        area: Area,
527        db_number: u16,
528        start: u32,
529        data: &[u8],
530    ) -> Result<()> {
531        let items = [MultiWriteItem {
532            area,
533            db_number,
534            start,
535            data: Bytes::copy_from_slice(data),
536        }];
537        self.write_multi_vars(&items).await
538    }
539
540    pub async fn read_szl(&self, szl_id: u16, szl_index: u16) -> Result<SzlResponse> {
541        let payload = self.read_szl_payload(szl_id, szl_index).await?;
542        let mut b = payload;
543        Ok(SzlResponse::decode(&mut b)?)
544    }
545
546    /// Send a UserData SZL query and return the raw SZL data block payload
547    /// (starting with block_len, szl_id, szl_index, then the data).
548    async fn read_szl_payload(&self, szl_id: u16, szl_index: u16) -> Result<Bytes> {
549        let mut inner = self.inner.lock().await;
550        let pdu_ref = Self::next_pdu_ref(&mut inner);
551
552        let req = SzlRequest { szl_id, szl_index };
553        let mut param_buf = BytesMut::new();
554        req.encode(&mut param_buf);
555
556        Self::send_s7(
557            &mut inner,
558            param_buf.freeze(),
559            Bytes::new(),
560            pdu_ref,
561            PduType::UserData,
562        )
563        .await?;
564
565        let (header, mut body) = Self::recv_s7(&mut inner).await?;
566
567        // Skip the echoed param section
568        if body.remaining() < header.param_len as usize {
569            return Err(Error::UnexpectedResponse);
570        }
571        body.advance(header.param_len as usize);
572
573        // body is now the data section.
574        // Skip the 4-byte data envelope: return_code(1) + transport(1) + data_len(2)
575        if body.remaining() < 4 {
576            return Err(Error::UnexpectedResponse);
577        }
578        body.advance(4);
579
580        // Remaining is the SZL data block: block_len(2) + szl_id(2) + szl_ix(2) + data
581        Ok(body.copy_to_bytes(body.remaining()))
582    }
583
584    pub async fn read_clock(&self) -> Result<PlcDateTime> {
585        let mut inner = self.inner.lock().await;
586        let pdu_ref = Self::next_pdu_ref(&mut inner);
587        let mut param_buf = BytesMut::new();
588        param_buf.extend_from_slice(&[0x00, 0x01, 0x12, 0x04, 0xF5, 0x00]);
589        Self::send_s7(
590            &mut inner,
591            param_buf.freeze(),
592            Bytes::new(),
593            pdu_ref,
594            PduType::UserData,
595        )
596        .await?;
597        let (_header, mut body) = Self::recv_s7(&mut inner).await?;
598        if body.remaining() > 8 {
599            body.advance(body.remaining() - 8);
600        }
601        Ok(PlcDateTime::decode(&mut body)?)
602    }
603
604    /// Copy RAM data to ROM (function 0x43).
605    ///
606    /// Copies the CPU's work memory to its load memory (retain on power-off).
607    pub async fn copy_ram_to_rom(&self) -> Result<()> {
608        let mut inner = self.inner.lock().await;
609        let pdu_ref = Self::next_pdu_ref(&mut inner);
610        let param = Bytes::copy_from_slice(&[
611            0x00, 0x01, 0x12, 0x04, 0x43, 0x44, 0x01, 0x00,
612        ]);
613        Self::send_s7(&mut inner, param, Bytes::new(), pdu_ref, PduType::UserData).await?;
614        let (header, _body) = Self::recv_s7(&mut inner).await?;
615        check_plc_error(&header, "copy_ram_to_rom")?;
616        Ok(())
617    }
618
619    /// Compress the PLC work memory (function 0x42).
620    ///
621    /// Reorganises memory to eliminate fragmentation.  The PLC must be in STOP
622    /// mode before calling this.
623    pub async fn compress(&self) -> Result<()> {
624        let mut inner = self.inner.lock().await;
625        let pdu_ref = Self::next_pdu_ref(&mut inner);
626        let param = Bytes::copy_from_slice(&[
627            0x00, 0x01, 0x12, 0x04, 0x42, 0x44, 0x01, 0x00,
628        ]);
629        Self::send_s7(&mut inner, param, Bytes::new(), pdu_ref, PduType::UserData).await?;
630        let (header, _body) = Self::recv_s7(&mut inner).await?;
631        check_plc_error(&header, "compress")?;
632        Ok(())
633    }
634
635    // -- PLC control & status -------------------------------------------------
636
637    /// Send a simple Job with a 2-byte parameter (func + 0x00) and no data.
638    async fn simple_control(inner: &mut Inner<T>, pdu_ref: u16, func: u8) -> Result<()> {
639        let param = Bytes::copy_from_slice(&[func, 0x00]);
640        Self::send_s7(inner, param, Bytes::new(), pdu_ref, PduType::Job).await?;
641        let (header, _body) = Self::recv_s7(inner).await?;
642        check_plc_error(&header, "plc_control")?;
643        Ok(())
644    }
645
646    /// Stop the PLC (S7 function code 0x29).
647    ///
648    /// Sends a Job request with no additional data. Returns `Ok(())` when the
649    /// PLC acknowledges the command, or an error if the PLC rejects it
650    /// (e.g., password-protected or CPU in a non-stoppable state).
651    pub async fn plc_stop(&self) -> Result<()> {
652        let mut inner = self.inner.lock().await;
653        let pdu_ref = Self::next_pdu_ref(&mut inner);
654        Self::simple_control(&mut inner, pdu_ref, 0x29).await
655    }
656
657    /// Hot-start (warm restart) the PLC (S7 function code 0x28).
658    ///
659    /// A warm restart retains the DB content and retentive memory.
660    pub async fn plc_hot_start(&self) -> Result<()> {
661        let mut inner = self.inner.lock().await;
662        let pdu_ref = Self::next_pdu_ref(&mut inner);
663        Self::simple_control(&mut inner, pdu_ref, 0x28).await
664    }
665
666    /// Cold-start (full restart) the PLC (S7 function code 0x2A).
667    ///
668    /// A cold start clears all DBs and non-retentive memory.
669    pub async fn plc_cold_start(&self) -> Result<()> {
670        let mut inner = self.inner.lock().await;
671        let pdu_ref = Self::next_pdu_ref(&mut inner);
672        Self::simple_control(&mut inner, pdu_ref, 0x2A).await
673    }
674
675    /// Read the current PLC status (S7 function code 0x31).
676    ///
677    /// Returns one of [`PlcStatus::Run`], [`PlcStatus::Stop`], or
678    /// [`PlcStatus::Unknown`].
679    pub async fn get_plc_status(&self) -> Result<crate::types::PlcStatus> {
680        let mut inner = self.inner.lock().await;
681        let pdu_ref = Self::next_pdu_ref(&mut inner);
682        let param = Bytes::copy_from_slice(&[0x31, 0x00]);
683        Self::send_s7(&mut inner, param, Bytes::new(), pdu_ref, PduType::Job).await?;
684        let (header, mut body) = Self::recv_s7(&mut inner).await?;
685        check_plc_error(&header, "get_plc_status")?;
686        // Skip param echo: func (1) + reserved (1)
687        if body.remaining() >= 2 {
688            body.advance(2);
689        }
690        if body.remaining() < 1 {
691            return Err(Error::UnexpectedResponse);
692        }
693        let status_byte = body.get_u8();
694        match status_byte {
695            0x00 => Ok(crate::types::PlcStatus::Unknown),
696            0x04 => Ok(crate::types::PlcStatus::Stop),
697            0x08 => Ok(crate::types::PlcStatus::Run),
698            other => Err(Error::PlcError {
699                code: other as u32,
700                message: format!("unknown PLC status byte: 0x{other:02X}"),
701            }),
702        }
703    }
704
705    // -- PLC information queries (via SZL UserData) ---------------------------
706
707    /// Read the PLC order code (SZL ID 0x0011).
708    ///
709    /// The order code is a 20-character ASCII string (e.g. `"6ES7 317-2EK14-0AB0"`).
710    pub async fn get_order_code(&self) -> Result<crate::types::OrderCode> {
711        let payload = self.read_szl_payload(0x0011, 0x0000).await?;
712        if payload.len() < 8 {
713            return Err(Error::UnexpectedResponse);
714        }
715        let mut b = payload;
716        let _block_len = b.get_u16();
717        let _szl_id = b.get_u16();
718        let _szl_ix = b.get_u16();
719        // Order code is the first 20 bytes of SZL data, space-padded
720        let code_bytes = &b[..b.len().min(20)];
721        let code = String::from_utf8_lossy(code_bytes).trim().to_string();
722        Ok(crate::types::OrderCode { code })
723    }
724
725    /// Read detailed CPU information (SZL ID 0x001C).
726    ///
727    /// Returns module type, serial number, plant identification, copyright
728    /// and module name fields pre-parsed from the SZL response.
729    pub async fn get_cpu_info(&self) -> Result<crate::types::CpuInfo> {
730        let payload = self.read_szl_payload(0x001C, 0x0000).await?;
731        if payload.len() < 8 {
732            return Err(Error::UnexpectedResponse);
733        }
734        let mut b = payload;
735        let _block_len = b.get_u16();
736        let _szl_id = b.get_u16();
737        let _szl_ix = b.get_u16();
738
739        // SZL 0x001C response layout (each field is left-aligned, space-padded):
740        //   [0..24]  Module type name
741        //   [24..48] Serial number
742        //   [48..72] Plant identification (AS name)
743        //   [72..98] Copyright (26 bytes)
744        //   [98..122] Module name (24 bytes)
745        let data = &b[..b.len().min(122)];
746        let module_type = String::from_utf8_lossy(&data[0..24.min(data.len())]).trim().to_string();
747        let serial_number = String::from_utf8_lossy(&data[24..48.min(data.len())]).trim().to_string();
748        let as_name = String::from_utf8_lossy(&data[48..72.min(data.len())]).trim().to_string();
749        let copyright = String::from_utf8_lossy(&data[72..98.min(data.len())]).trim().to_string();
750        let module_name = String::from_utf8_lossy(&data[98..122.min(data.len())]).trim().to_string();
751        Ok(crate::types::CpuInfo {
752            module_type,
753            serial_number,
754            as_name,
755            copyright,
756            module_name,
757        })
758    }
759
760    /// Read communication processor information (SZL ID 0x0131).
761    ///
762    /// Returns maximum PDU length, connection count, and baud rates.
763    pub async fn get_cp_info(&self) -> Result<crate::types::CpInfo> {
764        let payload = self.read_szl_payload(0x0131, 0x0000).await?;
765        if payload.len() < 14 {
766            return Err(Error::UnexpectedResponse);
767        }
768        let mut b = payload;
769        let _block_len = b.get_u16();
770        let _szl_id = b.get_u16();
771        let _szl_ix = b.get_u16();
772        // Next 16 bytes: 4 × u32 big-endian
773        let max_pdu_len = b.get_u32();
774        let max_connections = b.get_u32();
775        let max_mpi_rate = b.get_u32();
776        let max_bus_rate = b.get_u32();
777        Ok(crate::types::CpInfo {
778            max_pdu_len,
779            max_connections,
780            max_mpi_rate,
781            max_bus_rate,
782        })
783    }
784
785    /// Read the rack module list (SZL ID 0x00A0).
786    ///
787    /// Each entry is a 2-byte module type identifier.
788    pub async fn read_module_list(&self) -> Result<Vec<crate::types::ModuleEntry>> {
789        let payload = self.read_szl_payload(0x00A0, 0x0000).await?;
790        if payload.len() < 6 {
791            return Err(Error::UnexpectedResponse);
792        }
793        let mut b = payload;
794        let _block_len = b.get_u16();
795        let _szl_id = b.get_u16();
796        let _szl_ix = b.get_u16();
797        let mut modules = Vec::new();
798        while b.remaining() >= 2 {
799            modules.push(crate::types::ModuleEntry {
800                module_type: b.get_u16(),
801            });
802        }
803        Ok(modules)
804    }
805
806    // -- Block list & block info (via SZL + UserData) -------------------------
807
808    /// List all blocks in the PLC grouped by type (SZL 0x0130).
809    ///
810    /// Returns a [`BlockList`] with the total block count and per-type entries.
811    pub async fn list_blocks(&self) -> Result<crate::types::BlockList> {
812        let payload = self.read_szl_payload(0x0130, 0x0000).await?;
813        if payload.len() < 10 {
814            return Err(Error::UnexpectedResponse);
815        }
816        let mut b = payload;
817        let _block_len = b.get_u16();
818        let _szl_id = b.get_u16();
819        let _szl_ix = b.get_u16();
820        let total_count = b.get_u32();
821        let mut entries = Vec::new();
822        while b.remaining() >= 4 {
823            entries.push(crate::types::BlockListEntry {
824                block_type: b.get_u16(),
825                count: b.get_u16(),
826            });
827        }
828        Ok(crate::types::BlockList {
829            total_count,
830            entries,
831        })
832    }
833
834    /// Internal: send a UserData block-info request and return the raw response
835    /// data section payload (4-byte envelope skipped).
836    async fn block_info_query(
837        &self,
838        func: u8,
839        block_type: u8,
840        block_number: u16,
841    ) -> Result<Bytes> {
842        let mut inner = self.inner.lock().await;
843        let pdu_ref = Self::next_pdu_ref(&mut inner);
844
845        // UserData param for block info (function 0x13 or 0x14):
846        //   [8-byte header] [block_type(1)] [0x00] [block_number(2)]
847        let mut param_buf = BytesMut::with_capacity(12);
848        param_buf.extend_from_slice(&[
849            0x00, 0x01, 0x12, 0x04, func, 0x44, 0x01, 0x00,
850            block_type, 0x00,
851        ]);
852        param_buf.put_u16(block_number);
853
854        Self::send_s7(
855            &mut inner,
856            param_buf.freeze(),
857            Bytes::new(),
858            pdu_ref,
859            PduType::UserData,
860        )
861        .await?;
862
863        let (header, mut body) = Self::recv_s7(&mut inner).await?;
864
865        // Skip echoed param section
866        if body.remaining() < header.param_len as usize {
867            return Err(Error::UnexpectedResponse);
868        }
869        body.advance(header.param_len as usize);
870
871        // Skip 4-byte data envelope (return_code, transport, data_len)
872        if body.remaining() < 4 {
873            return Err(Error::UnexpectedResponse);
874        }
875        body.advance(4);
876
877        Ok(body.copy_to_bytes(body.remaining()))
878    }
879
880    /// Get detailed information about a block stored on the PLC.
881    ///
882    /// `block_type` should be one of the [`BlockType`](crate::types::BlockType)
883    /// discriminant values (e.g. `0x41` for DB, `0x38` for OB).
884    pub async fn get_ag_block_info(
885        &self,
886        block_type: u8,
887        block_number: u16,
888    ) -> Result<crate::types::BlockInfo> {
889        self.get_block_info(0x13, block_type, block_number).await
890    }
891
892    /// Get detailed block information from the PG perspective.
893    ///
894    /// Same fields as [`get_ag_block_info`](Self::get_ag_block_info) but the
895    /// information is from the programming-device viewpoint.
896    pub async fn get_pg_block_info(
897        &self,
898        block_type: u8,
899        block_number: u16,
900    ) -> Result<crate::types::BlockInfo> {
901        self.get_block_info(0x14, block_type, block_number).await
902    }
903
904    /// Shared implementation for AG and PG block info.
905    async fn get_block_info(
906        &self,
907        func: u8,
908        block_type: u8,
909        block_number: u16,
910    ) -> Result<crate::types::BlockInfo> {
911        let payload = self
912            .block_info_query(func, block_type, block_number)
913            .await?;
914        // Minimum for a valid block info: 6-byte header + block_type + block_number + language + flags + ...
915        if payload.len() < 24 {
916            return Err(Error::UnexpectedResponse);
917        }
918        let mut b = payload;
919
920        // Parse block info response (field order derived from S7 protocol):
921        let _blk_type_hi = b.get_u16(); // may echo block type as u16
922        let blk_number = b.get_u16();
923        let language = b.get_u16();
924        let flags = b.get_u16();
925        let mc7_size = b.get_u16();
926        let _size_lo = b.get_u16(); // load-memory size low word
927        let size_ram = b.get_u16();
928        let _size_ro = b.get_u16(); // 0 or RO-size
929        let local_data = b.get_u16();
930        let checksum = b.get_u16();
931        let version = b.get_u16();
932
933        // String fields: author(8), family(8), header(20?), date(8)
934        let author = if b.remaining() >= 8 {
935            String::from_utf8_lossy(&b[..8]).trim_end_matches('\0').trim().to_string()
936        } else { String::new() };
937        b.advance(8.min(b.remaining()));
938
939        let family = if b.remaining() >= 8 {
940            String::from_utf8_lossy(&b[..8]).trim_end_matches('\0').trim().to_string()
941        } else { String::new() };
942        b.advance(8.min(b.remaining()));
943
944        let header = if b.remaining() >= 20 {
945            String::from_utf8_lossy(&b[..20]).trim_end_matches('\0').trim().to_string()
946        } else { String::new() };
947        b.advance(20.min(b.remaining()));
948
949        let date = if b.remaining() >= 8 {
950            String::from_utf8_lossy(&b[..8]).trim_end_matches('\0').trim().to_string()
951        } else { String::new() };
952
953        // Reconstruct total size from the two size halves
954        let size = ((_blk_type_hi as u32) << 16) | (b.len() as u32 & 0xFFFF);
955        let size_u16 = size.min(0xFFFF) as u16;
956
957        Ok(crate::types::BlockInfo {
958            block_type: _blk_type_hi,
959            block_number: blk_number,
960            language,
961            flags,
962            size: size_u16,
963            size_ram,
964            mc7_size,
965            local_data,
966            checksum,
967            version,
968            author,
969            family,
970            header,
971            date,
972        })
973    }
974
975    // -- Security / protection (set/clear password + get protection) ----------
976
977    /// Set a session password for protected PLC access.
978    ///
979    /// The password is obfuscated using the S7 nibble-swap + XOR-0x55 algorithm
980    /// and sent as a Job PDU with function code 0x12.  Passwords longer than
981    /// 8 bytes are truncated.
982    pub async fn set_session_password(&self, password: &str) -> Result<()> {
983        let encrypted = crate::types::encrypt_password(password);
984        let mut inner = self.inner.lock().await;
985        let pdu_ref = Self::next_pdu_ref(&mut inner);
986        let param = Bytes::copy_from_slice(&[0x12, 0x00]);
987        let data = Bytes::copy_from_slice(&encrypted);
988        Self::send_s7(&mut inner, param, data, pdu_ref, PduType::Job).await?;
989        let (header, _body) = Self::recv_s7(&mut inner).await?;
990        check_plc_error(&header, "set_session_password")?;
991        Ok(())
992    }
993
994    /// Clear the session password on the PLC (function code 0x11).
995    pub async fn clear_session_password(&self) -> Result<()> {
996        let mut inner = self.inner.lock().await;
997        let pdu_ref = Self::next_pdu_ref(&mut inner);
998        let param = Bytes::copy_from_slice(&[0x11, 0x00]);
999        Self::send_s7(&mut inner, param, Bytes::new(), pdu_ref, PduType::Job).await?;
1000        let (header, _body) = Self::recv_s7(&mut inner).await?;
1001        check_plc_error(&header, "clear_session_password")?;
1002        Ok(())
1003    }
1004
1005    /// Read the current protection level (SZL ID 0x0032, index 0x0004).
1006    ///
1007    /// Returns the protection scheme identifiers and level;
1008    /// `password_set` is `true` when the PLC reports a non-empty password.
1009    pub async fn get_protection(&self) -> Result<crate::types::Protection> {
1010        let payload = self.read_szl_payload(0x0032, 0x0004).await?;
1011        if payload.len() < 14 {
1012            return Err(Error::UnexpectedResponse);
1013        }
1014        let mut b = payload;
1015        let _block_len = b.get_u16();
1016        let _szl_id = b.get_u16();
1017        let _szl_ix = b.get_u16();
1018        let scheme_szl = b.get_u16();
1019        let scheme_module = b.get_u16();
1020        let scheme_bus = b.get_u16();
1021        let level = b.get_u16();
1022        // Next 8 bytes = pass_word field ("PASSWORD" if set, spaces otherwise)
1023        let pass_wort = if b.remaining() >= 8 {
1024            String::from_utf8_lossy(&b[..8]).trim().to_string()
1025        } else {
1026            String::new()
1027        };
1028        let password_set = pass_wort.eq_ignore_ascii_case("PASSWORD");
1029        Ok(crate::types::Protection {
1030            scheme_szl,
1031            scheme_module,
1032            scheme_bus,
1033            level,
1034            password_set,
1035        })
1036    }
1037
1038    // -- Block upload / download / delete ------------------------------------
1039    //
1040    // S7 function 0x1D = Upload  (sub-fn: 0=start, 1=data, 2=end)
1041    // S7 function 0x1E = Download (sub-fn: 0=start, 1=data, 2=end)
1042    // S7 function 0x1F = Delete
1043
1044    /// Delete a block from the PLC (S7 function code 0x1F).
1045    pub async fn delete_block(&self, block_type: u8, block_number: u16) -> Result<()> {
1046        let mut inner = self.inner.lock().await;
1047        let pdu_ref = Self::next_pdu_ref(&mut inner);
1048        // param: [0x1F, 0x00, block_type, 0x00, block_number(2)]
1049        let mut param = BytesMut::with_capacity(6);
1050        param.extend_from_slice(&[0x1F, 0x00, block_type, 0x00]);
1051        param.put_u16(block_number);
1052        Self::send_s7(
1053            &mut inner,
1054            param.freeze(),
1055            Bytes::new(),
1056            pdu_ref,
1057            PduType::Job,
1058        )
1059        .await?;
1060        let (header, _body) = Self::recv_s7(&mut inner).await?;
1061        check_plc_error(&header, "delete_block")?;
1062        Ok(())
1063    }
1064
1065    /// Upload a PLC block via S7 PI-Upload (function 0x1D).
1066    ///
1067    /// Returns the raw block bytes in Diagra format (20-byte header + payload).
1068    /// Use [`BlockData::from_bytes`] to parse the result.
1069    pub async fn upload(&self, block_type: u8, block_number: u16) -> Result<Vec<u8>> {
1070        let mut inner = self.inner.lock().await;
1071        let pdu_ref = Self::next_pdu_ref(&mut inner);
1072
1073        // --- Step 1: Start upload (sub-fn=0x00) ---
1074        // param: [0x1D, 0x00, block_type, 0x00, block_number(2)]
1075        let mut param = BytesMut::with_capacity(6);
1076        param.extend_from_slice(&[0x1D, 0x00, block_type, 0x00]);
1077        param.put_u16(block_number);
1078        Self::send_s7(
1079            &mut inner,
1080            param.freeze(),
1081            Bytes::new(),
1082            pdu_ref,
1083            PduType::Job,
1084        )
1085        .await?;
1086        let (header, mut body) = Self::recv_s7(&mut inner).await?;
1087        check_plc_error(&header, "upload_start")?;
1088        // Response data: [upload_id(4)][total_len(4)]
1089        if body.remaining() < 8 {
1090            return Err(Error::UnexpectedResponse);
1091        }
1092        if body.remaining() >= 2 {
1093            body.advance(2); // skip param echo
1094        }
1095        let upload_id = body.get_u32();
1096        let _total_len = body.get_u32();
1097
1098        // --- Step 2: Loop data chunks (sub-fn=0x01) ---
1099        let mut block_data = Vec::new();
1100        loop {
1101            let chunk_pdu_ref = Self::next_pdu_ref(&mut inner);
1102            let mut dparam = BytesMut::with_capacity(6);
1103            dparam.extend_from_slice(&[0x1D, 0x01]);
1104            dparam.put_u32(upload_id);
1105            Self::send_s7(
1106                &mut inner,
1107                dparam.freeze(),
1108                Bytes::new(),
1109                chunk_pdu_ref,
1110                PduType::Job,
1111            )
1112            .await?;
1113            let (dheader, mut dbody) = Self::recv_s7(&mut inner).await?;
1114            check_plc_error(&dheader, "upload_data")?;
1115            // Skip param echo
1116            if dbody.remaining() >= 2 {
1117                dbody.advance(2);
1118            }
1119            if dbody.is_empty() {
1120                break; // no more data
1121            }
1122            // The first data PDU may have a 4-byte "data header" before the actual block data
1123            // (return_code + transport + bit_len).  Skip it.
1124            if block_data.is_empty() && dbody.remaining() >= 4 {
1125                // Peek at the first byte — if it looks like a return_code (0xFF), skip 4
1126                if dbody[0] == 0xFF || dbody[0] == 0x00 {
1127                    dbody.advance(4);
1128                }
1129            }
1130            let chunk = dbody.copy_to_bytes(dbody.remaining());
1131            block_data.extend_from_slice(&chunk);
1132
1133            // If this chunk was smaller than PDU size, it's the last one
1134            if chunk.len() < inner.connection.pdu_size as usize - 50 {
1135                break;
1136            }
1137            // Safety: prevent infinite loop on broken PLC
1138            if block_data.len() > 1024 * 1024 * 4 {
1139                // 4 MB
1140                return Err(Error::UnexpectedResponse);
1141            }
1142        }
1143
1144        // --- Step 3: End upload (sub-fn=0x02) ---
1145        let end_pdu_ref = Self::next_pdu_ref(&mut inner);
1146        let mut eparam = BytesMut::with_capacity(6);
1147        eparam.extend_from_slice(&[0x1D, 0x02]);
1148        eparam.put_u32(upload_id);
1149        Self::send_s7(
1150            &mut inner,
1151            eparam.freeze(),
1152            Bytes::new(),
1153            end_pdu_ref,
1154            PduType::Job,
1155        )
1156        .await?;
1157        let (eheader, _ebody) = Self::recv_s7(&mut inner).await?;
1158        check_plc_error(&eheader, "upload_end")?;
1159
1160        Ok(block_data)
1161    }
1162
1163    /// Upload a DB block (convenience wrapper around [`upload`](Self::upload)).
1164    pub async fn db_get(&self, db_number: u16) -> Result<Vec<u8>> {
1165        self.upload(0x41, db_number).await // Block_DB = 0x41
1166    }
1167
1168    /// Download a block to the PLC (S7 function 0x1E).
1169    ///
1170    /// `data` should be in Diagra format (20-byte header + payload, as returned by
1171    /// [`upload`](Self::upload) or built via [`BlockData::to_bytes`]).
1172    pub async fn download(&self, block_type: u8, block_number: u16, data: &[u8]) -> Result<()> {
1173        let total_len = data.len() as u32;
1174        let mut inner = self.inner.lock().await;
1175        let pdu_avail = (inner.connection.pdu_size as usize).saturating_sub(50);
1176
1177        // --- Step 1: Start download (sub-fn=0x00) ---
1178        let start_ref = Self::next_pdu_ref(&mut inner);
1179        // param: [0x1E, 0x00, block_type, 0x00, block_number(2), total_len(4)]
1180        let mut sparam = BytesMut::with_capacity(10);
1181        sparam.extend_from_slice(&[0x1E, 0x00, block_type, 0x00]);
1182        sparam.put_u16(block_number);
1183        sparam.put_u32(total_len);
1184
1185        // First data chunk
1186        let chunk_len = pdu_avail.min(data.len());
1187        let first_chunk = Bytes::copy_from_slice(&data[..chunk_len]);
1188        Self::send_s7(
1189            &mut inner,
1190            sparam.freeze(),
1191            first_chunk,
1192            start_ref,
1193            PduType::Job,
1194        )
1195        .await?;
1196
1197        let (sheader, mut sbody) = Self::recv_s7(&mut inner).await?;
1198        check_plc_error(&sheader, "download_start")?;
1199        // Response: [download_id(4)]
1200        if sbody.remaining() >= 2 {
1201            sbody.advance(2); // skip param echo
1202        }
1203        if sbody.remaining() < 4 {
1204            return Err(Error::UnexpectedResponse);
1205        }
1206        let download_id = sbody.get_u32();
1207
1208        let mut offset = chunk_len;
1209
1210        // --- Step 2: Send remaining data chunks (sub-fn=0x01) ---
1211        while offset < data.len() {
1212            let chunk_ref = Self::next_pdu_ref(&mut inner);
1213            let end = (offset + pdu_avail).min(data.len());
1214            let chunk = Bytes::copy_from_slice(&data[offset..end]);
1215
1216            let mut dparam = BytesMut::with_capacity(6);
1217            dparam.extend_from_slice(&[0x1E, 0x01]);
1218            dparam.put_u32(download_id);
1219
1220            Self::send_s7(
1221                &mut inner,
1222                dparam.freeze(),
1223                chunk,
1224                chunk_ref,
1225                PduType::Job,
1226            )
1227            .await?;
1228
1229            let (dheader, _dbody) = Self::recv_s7(&mut inner).await?;
1230            check_plc_error(&dheader, "download_data")?;
1231            offset = end;
1232        }
1233
1234        // --- Step 3: End download (sub-fn=0x02) ---
1235        let end_ref = Self::next_pdu_ref(&mut inner);
1236        let mut eparam = BytesMut::with_capacity(6);
1237        eparam.extend_from_slice(&[0x1E, 0x02]);
1238        eparam.put_u32(download_id);
1239        Self::send_s7(
1240            &mut inner,
1241            eparam.freeze(),
1242            Bytes::new(),
1243            end_ref,
1244            PduType::Job,
1245        )
1246        .await?;
1247        let (eheader, _ebody) = Self::recv_s7(&mut inner).await?;
1248        check_plc_error(&eheader, "download_end")?;
1249
1250        Ok(())
1251    }
1252
1253    /// Fill a DB with a constant byte value.
1254    ///
1255    /// Uses [`get_ag_block_info`](Self::get_ag_block_info) to determine the DB
1256    /// size, then writes every byte to `value`.
1257    pub async fn db_fill(&self, db_number: u16, value: u8) -> Result<()> {
1258        let info = self.get_ag_block_info(0x41, db_number).await?; // Block_DB = 0x41
1259        let size = info.size as usize;
1260        if size == 0 {
1261            return Err(Error::PlcError {
1262                code: 0,
1263                message: format!("DB{db_number} has zero size"),
1264            });
1265        }
1266        let data = vec![value; size];
1267        // Write in chunks to respect PDU limits
1268        let chunk_size = 240usize; // conservative
1269        for offset in (0..size).step_by(chunk_size) {
1270            let end = (offset + chunk_size).min(size);
1271            self.db_write(db_number, offset as u32, &data[offset..end])
1272                .await?;
1273        }
1274        Ok(())
1275    }
1276}
1277
1278fn check_plc_error(header: &S7Header, context: &str) -> Result<()> {
1279    if let (Some(ec), Some(ecd)) = (header.error_class, header.error_code) {
1280        if ec != 0 || ecd != 0 {
1281            return Err(Error::PlcError {
1282                code: ((ec as u32) << 8) | ecd as u32,
1283                message: format!("{} error", context),
1284            });
1285        }
1286    }
1287    Ok(())
1288}
1289
1290impl S7Client<crate::transport::TcpTransport> {
1291    pub async fn connect(addr: SocketAddr, params: ConnectParams) -> Result<Self> {
1292        let transport =
1293            crate::transport::TcpTransport::connect(addr, params.connect_timeout).await?;
1294        Self::from_transport(transport, params).await
1295    }
1296}
1297
1298impl S7Client<crate::UdpTransport> {
1299    /// Connect to a PLC using UDP transport.
1300    pub async fn connect_udp(addr: SocketAddr, params: ConnectParams) -> Result<Self> {
1301        let transport = crate::UdpTransport::connect(addr)
1302            .await
1303            .map_err(Error::Io)?;
1304        Self::from_transport(transport, params).await
1305    }
1306}
1307
1308#[cfg(test)]
1309mod tests {
1310    use super::*;
1311    use bytes::BufMut;
1312    use crate::proto::{
1313        cotp::CotpPdu,
1314        s7::{
1315            header::{PduType, S7Header},
1316            negotiate::NegotiateResponse,
1317        },
1318        tpkt::TpktFrame,
1319    };
1320    use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
1321
1322    async fn mock_plc_db_read(mut server_io: tokio::io::DuplexStream, response_data: Vec<u8>) {
1323        let mut buf = vec![0u8; 4096];
1324
1325        // respond to COTP CR
1326        let _ = server_io.read(&mut buf).await;
1327        let cc = CotpPdu::ConnectConfirm {
1328            dst_ref: 1,
1329            src_ref: 1,
1330        };
1331        let mut cb = BytesMut::new();
1332        cc.encode(&mut cb);
1333        let mut tb = BytesMut::new();
1334        TpktFrame {
1335            payload: cb.freeze(),
1336        }
1337        .encode(&mut tb)
1338        .unwrap();
1339        server_io.write_all(&tb).await.unwrap();
1340
1341        // respond to S7 negotiate
1342        let _ = server_io.read(&mut buf).await;
1343        let neg = NegotiateResponse {
1344            max_amq_calling: 1,
1345            max_amq_called: 1,
1346            pdu_length: 480,
1347        };
1348        let mut s7b = BytesMut::new();
1349        S7Header {
1350            pdu_type: PduType::AckData,
1351            reserved: 0,
1352            pdu_ref: 1,
1353            param_len: 8,
1354            data_len: 0,
1355            error_class: Some(0),
1356            error_code: Some(0),
1357        }
1358        .encode(&mut s7b);
1359        neg.encode(&mut s7b);
1360        let dt = CotpPdu::Data {
1361            tpdu_nr: 0,
1362            last: true,
1363            payload: s7b.freeze(),
1364        };
1365        let mut cb = BytesMut::new();
1366        dt.encode(&mut cb);
1367        let mut tb = BytesMut::new();
1368        TpktFrame {
1369            payload: cb.freeze(),
1370        }
1371        .encode(&mut tb)
1372        .unwrap();
1373        server_io.write_all(&tb).await.unwrap();
1374
1375        // respond to db_read
1376        let _ = server_io.read(&mut buf).await;
1377        let mut s7b = BytesMut::new();
1378        S7Header {
1379            pdu_type: PduType::AckData,
1380            reserved: 0,
1381            pdu_ref: 2,
1382            param_len: 2,
1383            data_len: (4 + response_data.len()) as u16,
1384            error_class: Some(0),
1385            error_code: Some(0),
1386        }
1387        .encode(&mut s7b);
1388        s7b.extend_from_slice(&[0x04, 0x01]); // ReadVar func + 1 item
1389        s7b.put_u8(0xFF); // return_code = success
1390        s7b.put_u8(0x04); // transport = word
1391        s7b.put_u16((response_data.len() * 8) as u16);
1392        s7b.extend_from_slice(&response_data);
1393        let dt = CotpPdu::Data {
1394            tpdu_nr: 0,
1395            last: true,
1396            payload: s7b.freeze(),
1397        };
1398        let mut cb = BytesMut::new();
1399        dt.encode(&mut cb);
1400        let mut tb = BytesMut::new();
1401        TpktFrame {
1402            payload: cb.freeze(),
1403        }
1404        .encode(&mut tb)
1405        .unwrap();
1406        server_io.write_all(&tb).await.unwrap();
1407    }
1408
1409    #[tokio::test]
1410    async fn db_read_returns_data() {
1411        let (client_io, server_io) = duplex(4096);
1412        let params = ConnectParams::default();
1413        let expected = vec![0xDE, 0xAD, 0xBE, 0xEF];
1414        tokio::spawn(mock_plc_db_read(server_io, expected.clone()));
1415        let client = S7Client::from_transport(client_io, params).await.unwrap();
1416        let data = client.db_read(1, 0, 4).await.unwrap();
1417        assert_eq!(&data[..], &expected[..]);
1418    }
1419
1420    /// Mock that handles COTP+Negotiate handshake then serves one multi-read response.
1421    async fn mock_plc_multi_read(
1422        mut server_io: tokio::io::DuplexStream,
1423        items: Vec<Vec<u8>>, // one byte vec per item
1424    ) {
1425        let mut buf = vec![0u8; 4096];
1426
1427        // COTP CR
1428        let _ = server_io.read(&mut buf).await;
1429        let cc = CotpPdu::ConnectConfirm { dst_ref: 1, src_ref: 1 };
1430        let mut cb = BytesMut::new();
1431        cc.encode(&mut cb);
1432        let mut tb = BytesMut::new();
1433        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1434        server_io.write_all(&tb).await.unwrap();
1435
1436        // S7 Negotiate
1437        let _ = server_io.read(&mut buf).await;
1438        let neg = NegotiateResponse { max_amq_calling: 1, max_amq_called: 1, pdu_length: 480 };
1439        let mut s7b = BytesMut::new();
1440        S7Header {
1441            pdu_type: PduType::AckData, reserved: 0, pdu_ref: 1,
1442            param_len: 8, data_len: 0, error_class: Some(0), error_code: Some(0),
1443        }.encode(&mut s7b);
1444        neg.encode(&mut s7b);
1445        let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1446        let mut cb = BytesMut::new(); dt.encode(&mut cb);
1447        let mut tb = BytesMut::new();
1448        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1449        server_io.write_all(&tb).await.unwrap();
1450
1451        // ReadMultiVar request
1452        let _ = server_io.read(&mut buf).await;
1453
1454        // Build response data: one DataItem per input item
1455        let item_count = items.len() as u8;
1456        let mut data_bytes = BytesMut::new();
1457        for item_data in &items {
1458            data_bytes.put_u8(0xFF); // return_code OK
1459            data_bytes.put_u8(0x04); // transport byte
1460            data_bytes.put_u16((item_data.len() * 8) as u16);
1461            data_bytes.extend_from_slice(item_data);
1462            if item_data.len() % 2 != 0 {
1463                data_bytes.put_u8(0x00); // pad
1464            }
1465        }
1466        let data_len = data_bytes.len() as u16;
1467        let mut s7b = BytesMut::new();
1468        S7Header {
1469            pdu_type: PduType::AckData, reserved: 0, pdu_ref: 2,
1470            param_len: 2, data_len, error_class: Some(0), error_code: Some(0),
1471        }.encode(&mut s7b);
1472        s7b.extend_from_slice(&[0x04, item_count]); // func + item_count
1473        s7b.extend_from_slice(&data_bytes);
1474
1475        let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1476        let mut cb = BytesMut::new(); dt.encode(&mut cb);
1477        let mut tb = BytesMut::new();
1478        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1479        server_io.write_all(&tb).await.unwrap();
1480    }
1481
1482    #[tokio::test]
1483    async fn read_multi_vars_returns_all_items() {
1484        let (client_io, server_io) = duplex(4096);
1485        let params = ConnectParams::default();
1486        let item1 = vec![0xDE, 0xAD, 0xBE, 0xEF];
1487        let item2 = vec![0x01, 0x02];
1488        tokio::spawn(mock_plc_multi_read(server_io, vec![item1.clone(), item2.clone()]));
1489        let client = S7Client::from_transport(client_io, params).await.unwrap();
1490        let items = [MultiReadItem::db(1, 0, 4), MultiReadItem::db(2, 10, 2)];
1491        let results = client.read_multi_vars(&items).await.unwrap();
1492        assert_eq!(results.len(), 2);
1493        assert_eq!(&results[0][..], &item1[..]);
1494        assert_eq!(&results[1][..], &item2[..]);
1495    }
1496
1497    #[tokio::test]
1498    async fn read_multi_vars_empty_returns_empty() {
1499        let (client_io, server_io) = duplex(4096);
1500        let params = ConnectParams::default();
1501        tokio::spawn(mock_plc_multi_read(server_io, vec![]));
1502        let client = S7Client::from_transport(client_io, params).await.unwrap();
1503        let results = client.read_multi_vars(&[]).await.unwrap();
1504        assert!(results.is_empty());
1505    }
1506
1507    /// Mock that handles COTP+Negotiate then serves N write-response round-trips.
1508    /// `batches` is a list of item counts per round-trip; the mock sends 0xFF for each.
1509    async fn mock_plc_multi_write(
1510        mut server_io: tokio::io::DuplexStream,
1511        pdu_size: u16,
1512        batches: Vec<usize>,
1513    ) {
1514        let mut buf = vec![0u8; 65536];
1515
1516        // COTP CR
1517        let _ = server_io.read(&mut buf).await;
1518        let cc = CotpPdu::ConnectConfirm { dst_ref: 1, src_ref: 1 };
1519        let mut cb = BytesMut::new(); cc.encode(&mut cb);
1520        let mut tb = BytesMut::new();
1521        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1522        server_io.write_all(&tb).await.unwrap();
1523
1524        // S7 Negotiate
1525        let _ = server_io.read(&mut buf).await;
1526        let neg = NegotiateResponse { max_amq_calling: 1, max_amq_called: 1, pdu_length: pdu_size };
1527        let mut s7b = BytesMut::new();
1528        S7Header {
1529            pdu_type: PduType::AckData, reserved: 0, pdu_ref: 1,
1530            param_len: 8, data_len: 0, error_class: Some(0), error_code: Some(0),
1531        }.encode(&mut s7b);
1532        neg.encode(&mut s7b);
1533        let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1534        let mut cb = BytesMut::new(); dt.encode(&mut cb);
1535        let mut tb = BytesMut::new();
1536        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1537        server_io.write_all(&tb).await.unwrap();
1538
1539        // One round-trip per batch
1540        for (i, item_count) in batches.iter().enumerate() {
1541            let _ = server_io.read(&mut buf).await;
1542            // WriteVar response: param = func(0x05) + count; data = return_code per item
1543            let mut s7b = BytesMut::new();
1544            S7Header {
1545                pdu_type: PduType::AckData, reserved: 0, pdu_ref: (i + 2) as u16,
1546                param_len: 2, data_len: *item_count as u16,
1547                error_class: Some(0), error_code: Some(0),
1548            }.encode(&mut s7b);
1549            s7b.extend_from_slice(&[0x05, *item_count as u8]); // func + count
1550            for _ in 0..*item_count {
1551                s7b.put_u8(0xFF); // success
1552            }
1553            let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1554            let mut cb = BytesMut::new(); dt.encode(&mut cb);
1555            let mut tb = BytesMut::new();
1556            TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1557            server_io.write_all(&tb).await.unwrap();
1558        }
1559    }
1560
1561    #[tokio::test]
1562    async fn write_multi_vars_returns_ok() {
1563        let (client_io, server_io) = duplex(65536);
1564        let params = ConnectParams::default();
1565        tokio::spawn(mock_plc_multi_write(server_io, 480, vec![2]));
1566        let client = S7Client::from_transport(client_io, params).await.unwrap();
1567        let items = [
1568            MultiWriteItem::db(1, 0, vec![0xAA, 0xBB, 0xCC, 0xDD]),
1569            MultiWriteItem::db(2, 10, vec![0x01, 0x02]),
1570        ];
1571        client.write_multi_vars(&items).await.unwrap();
1572    }
1573
1574    #[tokio::test]
1575    async fn write_multi_vars_empty_returns_ok() {
1576        let (client_io, server_io) = duplex(4096);
1577        let params = ConnectParams::default();
1578        // No messages exchanged after handshake — the mock just needs to satisfy connect.
1579        tokio::spawn(mock_plc_multi_write(server_io, 480, vec![]));
1580        let client = S7Client::from_transport(client_io, params).await.unwrap();
1581        client.write_multi_vars(&[]).await.unwrap();
1582    }
1583
1584    /// Items split into two round-trips when PDU budget is exhausted.
1585    ///
1586    /// PDU = 64. max_payload = 64 - 10(hdr) - 2(overhead) = 52.
1587    /// Each item: 12(addr) + 4(data hdr) + 20(data) = 36.
1588    /// Two items = 72 > 52 → must split into two 1-item batches.
1589    #[tokio::test]
1590    async fn write_multi_vars_batches_when_pdu_limit_exceeded() {
1591        let (client_io, server_io) = duplex(65536);
1592        let params = ConnectParams::default();
1593        tokio::spawn(mock_plc_multi_write(server_io, 64, vec![1, 1]));
1594        let client = S7Client::from_transport(client_io, params).await.unwrap();
1595        let items = [
1596            MultiWriteItem::db(1, 0, vec![0x11u8; 20]),
1597            MultiWriteItem::db(2, 0, vec![0x22u8; 20]),
1598        ];
1599        client.write_multi_vars(&items).await.unwrap();
1600    }
1601
1602    /// Items are split into two round trips when response would exceed the negotiated PDU size.
1603    ///
1604    /// PDU = 64 bytes. max_resp_payload = 64 - 10(hdr) - 2(func+count) = 52 bytes.
1605    /// Each item with 30 bytes of data costs 4+30 = 34 bytes in the response.
1606    /// Two such items = 68 bytes → exceeds 52 → must split into 2 round trips.
1607    #[tokio::test]
1608    async fn read_multi_vars_batches_when_pdu_limit_exceeded() {
1609        use crate::proto::s7::negotiate::NegotiateResponse;
1610
1611        async fn mock_split_pdu(mut server_io: tokio::io::DuplexStream) {
1612            let mut buf = vec![0u8; 4096];
1613
1614            // COTP CR
1615            let _ = server_io.read(&mut buf).await;
1616            let cc = CotpPdu::ConnectConfirm { dst_ref: 1, src_ref: 1 };
1617            let mut cb = BytesMut::new(); cc.encode(&mut cb);
1618            let mut tb = BytesMut::new();
1619            TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1620            server_io.write_all(&tb).await.unwrap();
1621
1622            // Negotiate — PDU size 64
1623            let _ = server_io.read(&mut buf).await;
1624            let neg = NegotiateResponse {
1625                max_amq_calling: 1, max_amq_called: 1, pdu_length: 64,
1626            };
1627            let mut s7b = BytesMut::new();
1628            S7Header {
1629                pdu_type: PduType::AckData, reserved: 0, pdu_ref: 1,
1630                param_len: 8, data_len: 0, error_class: Some(0), error_code: Some(0),
1631            }.encode(&mut s7b);
1632            neg.encode(&mut s7b);
1633            let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1634            let mut cb = BytesMut::new(); dt.encode(&mut cb);
1635            let mut tb = BytesMut::new();
1636            TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1637            server_io.write_all(&tb).await.unwrap();
1638
1639            // Two separate round-trips, one item each
1640            let payloads: &[&[u8]] = &[&[0x11u8; 30], &[0x22u8; 30]];
1641            for (i, payload) in payloads.iter().enumerate() {
1642                let _ = server_io.read(&mut buf).await;
1643                let bit_len = (payload.len() * 8) as u16;
1644                let mut data_bytes = BytesMut::new();
1645                data_bytes.put_u8(0xFF);
1646                data_bytes.put_u8(0x04);
1647                data_bytes.put_u16(bit_len);
1648                data_bytes.extend_from_slice(payload);
1649                if payload.len() % 2 != 0 { data_bytes.put_u8(0x00); }
1650                let data_len = data_bytes.len() as u16;
1651                let mut s7b = BytesMut::new();
1652                S7Header {
1653                    pdu_type: PduType::AckData, reserved: 0, pdu_ref: (i + 2) as u16,
1654                    param_len: 2, data_len, error_class: Some(0), error_code: Some(0),
1655                }.encode(&mut s7b);
1656                s7b.extend_from_slice(&[0x04, 0x01]);
1657                s7b.extend_from_slice(&data_bytes);
1658                let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1659                let mut cb = BytesMut::new(); dt.encode(&mut cb);
1660                let mut tb = BytesMut::new();
1661                TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1662                server_io.write_all(&tb).await.unwrap();
1663            }
1664        }
1665
1666        let (client_io, server_io) = duplex(4096);
1667        let params = ConnectParams::default();
1668        tokio::spawn(mock_split_pdu(server_io));
1669        let client = S7Client::from_transport(client_io, params).await.unwrap();
1670
1671        let items = [MultiReadItem::db(1, 0, 30), MultiReadItem::db(2, 0, 30)];
1672        let results = client.read_multi_vars(&items).await.unwrap();
1673        assert_eq!(results.len(), 2);
1674        assert_eq!(&results[0][..], &[0x11u8; 30][..]);
1675        assert_eq!(&results[1][..], &[0x22u8; 30][..]);
1676    }
1677
1678    // -- PLC control & status mocks & tests -----------------------------------
1679
1680    /// Common handshake for control tests: COTP CR → CC, S7 Negotiate.
1681    async fn mock_handshake(server_io: &mut (impl AsyncRead + AsyncWrite + Unpin)) {
1682        let mut buf = vec![0u8; 4096];
1683
1684        // COTP CR
1685        let _ = server_io.read(&mut buf).await;
1686        let cc = CotpPdu::ConnectConfirm { dst_ref: 1, src_ref: 1 };
1687        let mut cb = BytesMut::new(); cc.encode(&mut cb);
1688        let mut tb = BytesMut::new();
1689        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1690        server_io.write_all(&tb).await.unwrap();
1691
1692        // S7 Negotiate
1693        let _ = server_io.read(&mut buf).await;
1694        let neg = NegotiateResponse { max_amq_calling: 1, max_amq_called: 1, pdu_length: 480 };
1695        let mut s7b = BytesMut::new();
1696        S7Header {
1697            pdu_type: PduType::AckData, reserved: 0, pdu_ref: 1,
1698            param_len: 8, data_len: 0, error_class: Some(0), error_code: Some(0),
1699        }.encode(&mut s7b);
1700        neg.encode(&mut s7b);
1701        let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1702        let mut cb = BytesMut::new(); dt.encode(&mut cb);
1703        let mut tb = BytesMut::new();
1704        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1705        server_io.write_all(&tb).await.unwrap();
1706    }
1707
1708    /// Mock for simple control commands (plc_stop / plc_hot_start / plc_cold_start).
1709    /// `ok` controls whether the mock sends success (error_class=0, error_code=0) or failure.
1710    async fn mock_plc_control(
1711        mut server_io: tokio::io::DuplexStream,
1712        ok: bool,
1713    ) {
1714        let mut buf = vec![0u8; 4096];
1715        mock_handshake(&mut server_io).await;
1716
1717        // Control request — consume
1718        let _ = server_io.read(&mut buf).await;
1719
1720        // AckData response
1721        let (ec, ecd) = if ok { (0u8, 0u8) } else { (0x81u8, 0x04u8) };
1722        let mut s7b = BytesMut::new();
1723        S7Header {
1724            pdu_type: PduType::AckData, reserved: 0, pdu_ref: 2,
1725            param_len: 0, data_len: 0,
1726            error_class: Some(ec), error_code: Some(ecd),
1727        }.encode(&mut s7b);
1728        let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1729        let mut cb = BytesMut::new(); dt.encode(&mut cb);
1730        let mut tb = BytesMut::new();
1731        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1732        server_io.write_all(&tb).await.unwrap();
1733    }
1734
1735    #[tokio::test]
1736    async fn plc_stop_succeeds() {
1737        let (client_io, server_io) = duplex(4096);
1738        let params = ConnectParams::default();
1739        tokio::spawn(mock_plc_control(server_io, true));
1740        let client = S7Client::from_transport(client_io, params).await.unwrap();
1741        client.plc_stop().await.unwrap();
1742    }
1743
1744    #[tokio::test]
1745    async fn plc_hot_start_succeeds() {
1746        let (client_io, server_io) = duplex(4096);
1747        let params = ConnectParams::default();
1748        tokio::spawn(mock_plc_control(server_io, true));
1749        let client = S7Client::from_transport(client_io, params).await.unwrap();
1750        client.plc_hot_start().await.unwrap();
1751    }
1752
1753    #[tokio::test]
1754    async fn plc_cold_start_succeeds() {
1755        let (client_io, server_io) = duplex(4096);
1756        let params = ConnectParams::default();
1757        tokio::spawn(mock_plc_control(server_io, true));
1758        let client = S7Client::from_transport(client_io, params).await.unwrap();
1759        client.plc_cold_start().await.unwrap();
1760    }
1761
1762    #[tokio::test]
1763    async fn plc_stop_rejected_returns_error() {
1764        let (client_io, server_io) = duplex(4096);
1765        let params = ConnectParams::default();
1766        tokio::spawn(mock_plc_control(server_io, false));
1767        let client = S7Client::from_transport(client_io, params).await.unwrap();
1768        let result = client.plc_stop().await;
1769        assert!(result.is_err());
1770    }
1771
1772    /// Mock for get_plc_status: sends back `status_byte` in the data section.
1773    async fn mock_plc_status(
1774        mut server_io: tokio::io::DuplexStream,
1775        status_byte: u8,
1776    ) {
1777        let mut buf = vec![0u8; 4096];
1778        mock_handshake(&mut server_io).await;
1779
1780        // GetPlcStatus request — consume
1781        let _ = server_io.read(&mut buf).await;
1782
1783        // Response: param echo [0x31, 0x00] + status byte
1784        let data = &[0x31u8, 0x00, status_byte]; // param(2) + data(1)
1785        let data_len = data.len() as u16;
1786        let mut s7b = BytesMut::new();
1787        S7Header {
1788            pdu_type: PduType::AckData, reserved: 0, pdu_ref: 2,
1789            param_len: 2, data_len,
1790            error_class: Some(0), error_code: Some(0),
1791        }.encode(&mut s7b);
1792        s7b.extend_from_slice(data);
1793        let dt = CotpPdu::Data { tpdu_nr: 0, last: true, payload: s7b.freeze() };
1794        let mut cb = BytesMut::new(); dt.encode(&mut cb);
1795        let mut tb = BytesMut::new();
1796        TpktFrame { payload: cb.freeze() }.encode(&mut tb).unwrap();
1797        server_io.write_all(&tb).await.unwrap();
1798    }
1799
1800    #[tokio::test]
1801    async fn get_plc_status_returns_run() {
1802        let (client_io, server_io) = duplex(4096);
1803        let params = ConnectParams::default();
1804        tokio::spawn(mock_plc_status(server_io, 0x08));
1805        let client = S7Client::from_transport(client_io, params).await.unwrap();
1806        let status = client.get_plc_status().await.unwrap();
1807        assert_eq!(status, crate::types::PlcStatus::Run);
1808    }
1809
1810    #[tokio::test]
1811    async fn get_plc_status_returns_stop() {
1812        let (client_io, server_io) = duplex(4096);
1813        let params = ConnectParams::default();
1814        tokio::spawn(mock_plc_status(server_io, 0x04));
1815        let client = S7Client::from_transport(client_io, params).await.unwrap();
1816        let status = client.get_plc_status().await.unwrap();
1817        assert_eq!(status, crate::types::PlcStatus::Stop);
1818    }
1819
1820    #[tokio::test]
1821    async fn get_plc_status_returns_unknown() {
1822        let (client_io, server_io) = duplex(4096);
1823        let params = ConnectParams::default();
1824        tokio::spawn(mock_plc_status(server_io, 0x00));
1825        let client = S7Client::from_transport(client_io, params).await.unwrap();
1826        let status = client.get_plc_status().await.unwrap();
1827        assert_eq!(status, crate::types::PlcStatus::Unknown);
1828    }
1829
1830    #[tokio::test]
1831    async fn get_plc_status_unknown_byte_returns_error() {
1832        let (client_io, server_io) = duplex(4096);
1833        let params = ConnectParams::default();
1834        tokio::spawn(mock_plc_status(server_io, 0xFF));
1835        let client = S7Client::from_transport(client_io, params).await.unwrap();
1836        let result = client.get_plc_status().await;
1837        assert!(result.is_err());
1838    }
1839}