Skip to main content

oxirs_modbus/protocol/
functions.rs

1//! Modbus function code PDU implementations
2//!
3//! Provides request/response structs with encode/decode for:
4//! - FC 0x01 — Read Coils
5//! - FC 0x02 — Read Discrete Inputs
6//! - FC 0x0F — Write Multiple Coils
7//! - FC 0x10 — Write Multiple Registers
8//!
9//! Quantity limits follow Modbus Application Protocol V1.1b3:
10//! - Read Coils / Discrete Inputs: 1–2000 bits
11//! - Write Multiple Coils: 1–1968 bits
12//! - Write Multiple Registers: 1–123 registers
13
14use crate::error::{ModbusError, ModbusResult};
15
16// ── constants ──────────────────────────────────────────────────────────────
17/// Maximum number of coils that may be read in one request.
18pub const MAX_READ_COILS: u16 = 2000;
19/// Maximum number of discrete inputs that may be read in one request.
20pub const MAX_READ_DISCRETE_INPUTS: u16 = 2000;
21/// Maximum number of coils that may be written in one request.
22pub const MAX_WRITE_COILS: u16 = 1968;
23/// Maximum number of registers that may be written in one request.
24pub const MAX_WRITE_REGISTERS: u16 = 123;
25
26// ── helpers ────────────────────────────────────────────────────────────────
27
28/// Pack a slice of booleans into a byte vector (LSB first, padded to byte boundary).
29pub fn pack_bits(bits: &[bool]) -> Vec<u8> {
30    let byte_count = (bits.len() + 7) / 8;
31    let mut bytes = vec![0u8; byte_count];
32    for (i, &bit) in bits.iter().enumerate() {
33        if bit {
34            bytes[i / 8] |= 1 << (i % 8);
35        }
36    }
37    bytes
38}
39
40/// Unpack `count` booleans from a packed byte slice (LSB first).
41pub fn unpack_bits(bytes: &[u8], count: usize) -> Vec<bool> {
42    let mut bits = Vec::with_capacity(count);
43    for i in 0..count {
44        let byte_idx = i / 8;
45        let bit_idx = i % 8;
46        let value = if byte_idx < bytes.len() {
47            (bytes[byte_idx] >> bit_idx) & 1 == 1
48        } else {
49            false
50        };
51        bits.push(value);
52    }
53    bits
54}
55
56// ── FC 0x01 — Read Coils ──────────────────────────────────────────────────
57
58/// PDU request for FC 0x01 Read Coils.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct ReadCoilsRequest {
61    /// Starting address of the first coil.
62    pub start_address: u16,
63    /// Number of coils to read (1–2000).
64    pub quantity: u16,
65}
66
67impl ReadCoilsRequest {
68    /// Create a new request, validating quantity.
69    pub fn new(start_address: u16, quantity: u16) -> ModbusResult<Self> {
70        if quantity == 0 || quantity > MAX_READ_COILS {
71            return Err(ModbusError::InvalidCount(quantity));
72        }
73        Ok(Self {
74            start_address,
75            quantity,
76        })
77    }
78
79    /// Encode to PDU bytes (function code + 4 data bytes).
80    pub fn encode(&self) -> Vec<u8> {
81        let mut buf = Vec::with_capacity(5);
82        buf.push(0x01);
83        buf.extend_from_slice(&self.start_address.to_be_bytes());
84        buf.extend_from_slice(&self.quantity.to_be_bytes());
85        buf
86    }
87
88    /// Decode from the PDU data bytes (after the function code has been stripped).
89    pub fn decode(data: &[u8]) -> ModbusResult<Self> {
90        if data.len() < 4 {
91            return Err(ModbusError::Io(std::io::Error::new(
92                std::io::ErrorKind::UnexpectedEof,
93                "ReadCoilsRequest: expected 4 data bytes",
94            )));
95        }
96        let start_address = u16::from_be_bytes([data[0], data[1]]);
97        let quantity = u16::from_be_bytes([data[2], data[3]]);
98        Self::new(start_address, quantity)
99    }
100}
101
102/// PDU response for FC 0x01 Read Coils.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub struct ReadCoilsResponse {
105    /// Status of each coil (in request order).
106    pub coil_status: Vec<bool>,
107}
108
109impl ReadCoilsResponse {
110    /// Create a response from a vector of boolean coil values.
111    pub fn new(coil_status: Vec<bool>) -> Self {
112        Self { coil_status }
113    }
114
115    /// Encode to PDU bytes (function code + byte count + packed bits).
116    pub fn encode(&self) -> Vec<u8> {
117        let packed = pack_bits(&self.coil_status);
118        let mut buf = Vec::with_capacity(2 + packed.len());
119        buf.push(0x01);
120        buf.push(packed.len() as u8);
121        buf.extend_from_slice(&packed);
122        buf
123    }
124
125    /// Decode from PDU data bytes (after function code stripped), given the expected coil count.
126    pub fn decode(data: &[u8], quantity: usize) -> ModbusResult<Self> {
127        if data.is_empty() {
128            return Err(ModbusError::Io(std::io::Error::new(
129                std::io::ErrorKind::UnexpectedEof,
130                "ReadCoilsResponse: missing byte count",
131            )));
132        }
133        let byte_count = data[0] as usize;
134        if data.len() < 1 + byte_count {
135            return Err(ModbusError::Io(std::io::Error::new(
136                std::io::ErrorKind::UnexpectedEof,
137                "ReadCoilsResponse: incomplete packed bytes",
138            )));
139        }
140        let packed = &data[1..1 + byte_count];
141        let coil_status = unpack_bits(packed, quantity);
142        Ok(Self { coil_status })
143    }
144}
145
146// ── FC 0x02 — Read Discrete Inputs ────────────────────────────────────────
147
148/// PDU request for FC 0x02 Read Discrete Inputs.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub struct ReadDiscreteInputsRequest {
151    /// Starting address of the first discrete input.
152    pub start_address: u16,
153    /// Number of discrete inputs to read (1–2000).
154    pub quantity: u16,
155}
156
157impl ReadDiscreteInputsRequest {
158    /// Create a new request, validating quantity.
159    pub fn new(start_address: u16, quantity: u16) -> ModbusResult<Self> {
160        if quantity == 0 || quantity > MAX_READ_DISCRETE_INPUTS {
161            return Err(ModbusError::InvalidCount(quantity));
162        }
163        Ok(Self {
164            start_address,
165            quantity,
166        })
167    }
168
169    /// Encode to PDU bytes.
170    pub fn encode(&self) -> Vec<u8> {
171        let mut buf = Vec::with_capacity(5);
172        buf.push(0x02);
173        buf.extend_from_slice(&self.start_address.to_be_bytes());
174        buf.extend_from_slice(&self.quantity.to_be_bytes());
175        buf
176    }
177
178    /// Decode from PDU data bytes (after function code stripped).
179    pub fn decode(data: &[u8]) -> ModbusResult<Self> {
180        if data.len() < 4 {
181            return Err(ModbusError::Io(std::io::Error::new(
182                std::io::ErrorKind::UnexpectedEof,
183                "ReadDiscreteInputsRequest: expected 4 data bytes",
184            )));
185        }
186        let start_address = u16::from_be_bytes([data[0], data[1]]);
187        let quantity = u16::from_be_bytes([data[2], data[3]]);
188        Self::new(start_address, quantity)
189    }
190}
191
192/// PDU response for FC 0x02 Read Discrete Inputs.
193#[derive(Debug, Clone, PartialEq, Eq)]
194pub struct ReadDiscreteInputsResponse {
195    /// Status of each discrete input (in request order).
196    pub input_status: Vec<bool>,
197}
198
199impl ReadDiscreteInputsResponse {
200    /// Create a response from a vector of boolean input values.
201    pub fn new(input_status: Vec<bool>) -> Self {
202        Self { input_status }
203    }
204
205    /// Encode to PDU bytes.
206    pub fn encode(&self) -> Vec<u8> {
207        let packed = pack_bits(&self.input_status);
208        let mut buf = Vec::with_capacity(2 + packed.len());
209        buf.push(0x02);
210        buf.push(packed.len() as u8);
211        buf.extend_from_slice(&packed);
212        buf
213    }
214
215    /// Decode from PDU data bytes (after function code stripped), given expected input count.
216    pub fn decode(data: &[u8], quantity: usize) -> ModbusResult<Self> {
217        if data.is_empty() {
218            return Err(ModbusError::Io(std::io::Error::new(
219                std::io::ErrorKind::UnexpectedEof,
220                "ReadDiscreteInputsResponse: missing byte count",
221            )));
222        }
223        let byte_count = data[0] as usize;
224        if data.len() < 1 + byte_count {
225            return Err(ModbusError::Io(std::io::Error::new(
226                std::io::ErrorKind::UnexpectedEof,
227                "ReadDiscreteInputsResponse: incomplete packed bytes",
228            )));
229        }
230        let packed = &data[1..1 + byte_count];
231        let input_status = unpack_bits(packed, quantity);
232        Ok(Self { input_status })
233    }
234}
235
236// ── FC 0x0F — Write Multiple Coils ────────────────────────────────────────
237
238/// PDU request for FC 0x0F Write Multiple Coils.
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub struct WriteMultipleCoilsRequest {
241    /// Starting address of the first coil to write.
242    pub start_address: u16,
243    /// Output values (1–1968).
244    pub outputs: Vec<bool>,
245}
246
247impl WriteMultipleCoilsRequest {
248    /// Create a new request, validating quantity.
249    pub fn new(start_address: u16, outputs: Vec<bool>) -> ModbusResult<Self> {
250        let qty = outputs.len() as u16;
251        if qty == 0 || qty > MAX_WRITE_COILS {
252            return Err(ModbusError::InvalidCount(qty));
253        }
254        Ok(Self {
255            start_address,
256            outputs,
257        })
258    }
259
260    /// Number of coils.
261    pub fn quantity(&self) -> u16 {
262        self.outputs.len() as u16
263    }
264
265    /// Encode to PDU bytes.
266    pub fn encode(&self) -> Vec<u8> {
267        let packed = pack_bits(&self.outputs);
268        let qty = self.outputs.len() as u16;
269        let mut buf = Vec::with_capacity(6 + packed.len());
270        buf.push(0x0F);
271        buf.extend_from_slice(&self.start_address.to_be_bytes());
272        buf.extend_from_slice(&qty.to_be_bytes());
273        buf.push(packed.len() as u8);
274        buf.extend_from_slice(&packed);
275        buf
276    }
277
278    /// Decode from PDU data bytes (after function code stripped).
279    pub fn decode(data: &[u8]) -> ModbusResult<Self> {
280        if data.len() < 5 {
281            return Err(ModbusError::Io(std::io::Error::new(
282                std::io::ErrorKind::UnexpectedEof,
283                "WriteMultipleCoilsRequest: too short",
284            )));
285        }
286        let start_address = u16::from_be_bytes([data[0], data[1]]);
287        let quantity = u16::from_be_bytes([data[2], data[3]]);
288        let byte_count = data[4] as usize;
289        if data.len() < 5 + byte_count {
290            return Err(ModbusError::Io(std::io::Error::new(
291                std::io::ErrorKind::UnexpectedEof,
292                "WriteMultipleCoilsRequest: incomplete packed bytes",
293            )));
294        }
295        let packed = &data[5..5 + byte_count];
296        let outputs = unpack_bits(packed, quantity as usize);
297        Self::new(start_address, outputs)
298    }
299}
300
301/// PDU response for FC 0x0F Write Multiple Coils.
302#[derive(Debug, Clone, PartialEq, Eq)]
303pub struct WriteMultipleCoilsResponse {
304    /// Starting address echoed from request.
305    pub start_address: u16,
306    /// Number of coils written.
307    pub quantity: u16,
308}
309
310impl WriteMultipleCoilsResponse {
311    /// Create a new response.
312    pub fn new(start_address: u16, quantity: u16) -> Self {
313        Self {
314            start_address,
315            quantity,
316        }
317    }
318
319    /// Encode to PDU bytes.
320    pub fn encode(&self) -> Vec<u8> {
321        let mut buf = Vec::with_capacity(5);
322        buf.push(0x0F);
323        buf.extend_from_slice(&self.start_address.to_be_bytes());
324        buf.extend_from_slice(&self.quantity.to_be_bytes());
325        buf
326    }
327
328    /// Decode from PDU data bytes (after function code stripped).
329    pub fn decode(data: &[u8]) -> ModbusResult<Self> {
330        if data.len() < 4 {
331            return Err(ModbusError::Io(std::io::Error::new(
332                std::io::ErrorKind::UnexpectedEof,
333                "WriteMultipleCoilsResponse: expected 4 data bytes",
334            )));
335        }
336        let start_address = u16::from_be_bytes([data[0], data[1]]);
337        let quantity = u16::from_be_bytes([data[2], data[3]]);
338        Ok(Self {
339            start_address,
340            quantity,
341        })
342    }
343}
344
345// ── FC 0x10 — Write Multiple Registers ───────────────────────────────────
346
347/// PDU request for FC 0x10 Write Multiple Registers.
348#[derive(Debug, Clone, PartialEq, Eq)]
349pub struct WriteMultipleRegistersRequest {
350    /// Starting address of the first register to write.
351    pub start_address: u16,
352    /// Register values to write (1–123).
353    pub values: Vec<u16>,
354}
355
356impl WriteMultipleRegistersRequest {
357    /// Create a new request, validating quantity.
358    pub fn new(start_address: u16, values: Vec<u16>) -> ModbusResult<Self> {
359        let qty = values.len() as u16;
360        if qty == 0 || qty > MAX_WRITE_REGISTERS {
361            return Err(ModbusError::InvalidCount(qty));
362        }
363        Ok(Self {
364            start_address,
365            values,
366        })
367    }
368
369    /// Number of registers.
370    pub fn quantity(&self) -> u16 {
371        self.values.len() as u16
372    }
373
374    /// Encode to PDU bytes.
375    pub fn encode(&self) -> Vec<u8> {
376        let qty = self.values.len() as u16;
377        let byte_count = (qty * 2) as u8;
378        let mut buf = Vec::with_capacity(6 + self.values.len() * 2);
379        buf.push(0x10);
380        buf.extend_from_slice(&self.start_address.to_be_bytes());
381        buf.extend_from_slice(&qty.to_be_bytes());
382        buf.push(byte_count);
383        for &v in &self.values {
384            buf.extend_from_slice(&v.to_be_bytes());
385        }
386        buf
387    }
388
389    /// Decode from PDU data bytes (after function code stripped).
390    pub fn decode(data: &[u8]) -> ModbusResult<Self> {
391        if data.len() < 5 {
392            return Err(ModbusError::Io(std::io::Error::new(
393                std::io::ErrorKind::UnexpectedEof,
394                "WriteMultipleRegistersRequest: too short",
395            )));
396        }
397        let start_address = u16::from_be_bytes([data[0], data[1]]);
398        let quantity = u16::from_be_bytes([data[2], data[3]]) as usize;
399        let byte_count = data[4] as usize;
400        if data.len() < 5 + byte_count {
401            return Err(ModbusError::Io(std::io::Error::new(
402                std::io::ErrorKind::UnexpectedEof,
403                "WriteMultipleRegistersRequest: incomplete register data",
404            )));
405        }
406        let mut values = Vec::with_capacity(quantity);
407        for i in 0..quantity {
408            let offset = 5 + i * 2;
409            if offset + 1 >= data.len() {
410                break;
411            }
412            values.push(u16::from_be_bytes([data[offset], data[offset + 1]]));
413        }
414        Self::new(start_address, values)
415    }
416}
417
418/// PDU response for FC 0x10 Write Multiple Registers.
419#[derive(Debug, Clone, PartialEq, Eq)]
420pub struct WriteMultipleRegistersResponse {
421    /// Starting address echoed from request.
422    pub start_address: u16,
423    /// Number of registers written.
424    pub quantity: u16,
425}
426
427impl WriteMultipleRegistersResponse {
428    /// Create a new response.
429    pub fn new(start_address: u16, quantity: u16) -> Self {
430        Self {
431            start_address,
432            quantity,
433        }
434    }
435
436    /// Encode to PDU bytes.
437    pub fn encode(&self) -> Vec<u8> {
438        let mut buf = Vec::with_capacity(5);
439        buf.push(0x10);
440        buf.extend_from_slice(&self.start_address.to_be_bytes());
441        buf.extend_from_slice(&self.quantity.to_be_bytes());
442        buf
443    }
444
445    /// Decode from PDU data bytes (after function code stripped).
446    pub fn decode(data: &[u8]) -> ModbusResult<Self> {
447        if data.len() < 4 {
448            return Err(ModbusError::Io(std::io::Error::new(
449                std::io::ErrorKind::UnexpectedEof,
450                "WriteMultipleRegistersResponse: expected 4 data bytes",
451            )));
452        }
453        let start_address = u16::from_be_bytes([data[0], data[1]]);
454        let quantity = u16::from_be_bytes([data[2], data[3]]);
455        Ok(Self {
456            start_address,
457            quantity,
458        })
459    }
460}
461
462// ── tests ──────────────────────────────────────────────────────────────────
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    // ── bit packing helpers ──────────────────────────────────────────────
468
469    #[test]
470    fn test_pack_bits_empty() {
471        assert!(pack_bits(&[]).is_empty());
472    }
473
474    #[test]
475    fn test_pack_bits_single_byte() {
476        // [T, F, T, F, T, F, T, F] → 0b01010101 = 0x55
477        let bits = [true, false, true, false, true, false, true, false];
478        assert_eq!(pack_bits(&bits), vec![0x55]);
479    }
480
481    #[test]
482    fn test_pack_bits_partial_byte() {
483        // [T, T, F] → 0b00000011 = 0x03
484        let bits = [true, true, false];
485        assert_eq!(pack_bits(&bits), vec![0x03]);
486    }
487
488    #[test]
489    fn test_pack_unpack_roundtrip() {
490        let original: Vec<bool> = (0..13).map(|i| i % 3 == 0).collect();
491        let packed = pack_bits(&original);
492        let unpacked = unpack_bits(&packed, original.len());
493        assert_eq!(unpacked, original);
494    }
495
496    // ── FC 0x01 Read Coils ───────────────────────────────────────────────
497
498    #[test]
499    fn test_read_coils_request_new_valid() {
500        let req = ReadCoilsRequest::new(100, 10).expect("valid");
501        assert_eq!(req.start_address, 100);
502        assert_eq!(req.quantity, 10);
503    }
504
505    #[test]
506    fn test_read_coils_request_zero_quantity_rejected() {
507        assert!(ReadCoilsRequest::new(0, 0).is_err());
508    }
509
510    #[test]
511    fn test_read_coils_request_max_quantity() {
512        assert!(ReadCoilsRequest::new(0, 2000).is_ok());
513    }
514
515    #[test]
516    fn test_read_coils_request_over_max_rejected() {
517        assert!(ReadCoilsRequest::new(0, 2001).is_err());
518    }
519
520    #[test]
521    fn test_read_coils_request_encode_decode() {
522        let req = ReadCoilsRequest::new(200, 16).expect("valid");
523        let encoded = req.encode();
524        // function code (1) + start_address (2) + quantity (2) = 5 bytes
525        assert_eq!(encoded.len(), 5);
526        assert_eq!(encoded[0], 0x01);
527        let decoded = ReadCoilsRequest::decode(&encoded[1..]).expect("decoded");
528        assert_eq!(decoded, req);
529    }
530
531    #[test]
532    fn test_read_coils_response_encode_decode() {
533        let status: Vec<bool> = vec![true, false, true, true, false, false, true, false, true];
534        let resp = ReadCoilsResponse::new(status.clone());
535        let encoded = resp.encode();
536        // function code (1) + byte_count (1) + 2 packed bytes = 4
537        assert_eq!(encoded[0], 0x01);
538        let decoded = ReadCoilsResponse::decode(&encoded[1..], status.len()).expect("decoded");
539        assert_eq!(decoded.coil_status, status);
540    }
541
542    #[test]
543    fn test_read_coils_response_aligned_bits() {
544        // Exactly 8 coils → 1 byte
545        let status = vec![true; 8];
546        let resp = ReadCoilsResponse::new(status.clone());
547        let encoded = resp.encode();
548        // function_code(1) + byte_count(1) + data(1) = 3
549        assert_eq!(encoded.len(), 3);
550        let decoded = ReadCoilsResponse::decode(&encoded[1..], 8).expect("ok");
551        assert_eq!(decoded.coil_status, status);
552    }
553
554    // ── FC 0x02 Read Discrete Inputs ────────────────────────────────────
555
556    #[test]
557    fn test_read_discrete_inputs_request_valid() {
558        let req = ReadDiscreteInputsRequest::new(0, 1).expect("valid");
559        assert_eq!(req.quantity, 1);
560    }
561
562    #[test]
563    fn test_read_discrete_inputs_request_zero_rejected() {
564        assert!(ReadDiscreteInputsRequest::new(0, 0).is_err());
565    }
566
567    #[test]
568    fn test_read_discrete_inputs_request_max() {
569        assert!(ReadDiscreteInputsRequest::new(0, 2000).is_ok());
570    }
571
572    #[test]
573    fn test_read_discrete_inputs_request_over_max() {
574        assert!(ReadDiscreteInputsRequest::new(0, 2001).is_err());
575    }
576
577    #[test]
578    fn test_read_discrete_inputs_encode_decode() {
579        let req = ReadDiscreteInputsRequest::new(50, 5).expect("valid");
580        let encoded = req.encode();
581        assert_eq!(encoded[0], 0x02);
582        let decoded = ReadDiscreteInputsRequest::decode(&encoded[1..]).expect("decoded");
583        assert_eq!(decoded, req);
584    }
585
586    #[test]
587    fn test_read_discrete_inputs_response_encode_decode() {
588        let status = vec![false, true, false, true, true];
589        let resp = ReadDiscreteInputsResponse::new(status.clone());
590        let encoded = resp.encode();
591        assert_eq!(encoded[0], 0x02);
592        let decoded = ReadDiscreteInputsResponse::decode(&encoded[1..], status.len()).expect("ok");
593        assert_eq!(decoded.input_status, status);
594    }
595
596    // ── FC 0x0F Write Multiple Coils ────────────────────────────────────
597
598    #[test]
599    fn test_write_multiple_coils_request_valid() {
600        let outputs = vec![true, false, true];
601        let req = WriteMultipleCoilsRequest::new(10, outputs.clone()).expect("valid");
602        assert_eq!(req.start_address, 10);
603        assert_eq!(req.quantity(), 3);
604    }
605
606    #[test]
607    fn test_write_multiple_coils_request_zero_rejected() {
608        assert!(WriteMultipleCoilsRequest::new(0, vec![]).is_err());
609    }
610
611    #[test]
612    fn test_write_multiple_coils_request_over_max_rejected() {
613        let too_many = vec![false; 1969];
614        assert!(WriteMultipleCoilsRequest::new(0, too_many).is_err());
615    }
616
617    #[test]
618    fn test_write_multiple_coils_encode_decode() {
619        let outputs: Vec<bool> = (0..10).map(|i| i % 2 == 0).collect();
620        let req = WriteMultipleCoilsRequest::new(0, outputs.clone()).expect("valid");
621        let encoded = req.encode();
622        assert_eq!(encoded[0], 0x0F);
623        let decoded = WriteMultipleCoilsRequest::decode(&encoded[1..]).expect("decoded");
624        assert_eq!(decoded.outputs, outputs);
625    }
626
627    #[test]
628    fn test_write_multiple_coils_response_encode_decode() {
629        let resp = WriteMultipleCoilsResponse::new(20, 15);
630        let encoded = resp.encode();
631        assert_eq!(encoded[0], 0x0F);
632        let decoded = WriteMultipleCoilsResponse::decode(&encoded[1..]).expect("decoded");
633        assert_eq!(decoded, resp);
634    }
635
636    #[test]
637    fn test_write_multiple_coils_bit_packing() {
638        // All-true outputs produce 0xFF bytes
639        let outputs = vec![true; 8];
640        let req = WriteMultipleCoilsRequest::new(0, outputs).expect("valid");
641        let encoded = req.encode();
642        // function(1) + start(2) + qty(2) + byte_count(1) + data(1) = 7
643        assert_eq!(encoded.len(), 7);
644        // Packed byte: all bits set
645        assert_eq!(encoded[6], 0xFF);
646    }
647
648    // ── FC 0x10 Write Multiple Registers ─────────────────────────────────
649
650    #[test]
651    fn test_write_multiple_registers_request_valid() {
652        let values = vec![100u16, 200, 300];
653        let req = WriteMultipleRegistersRequest::new(40001, values.clone()).expect("valid");
654        assert_eq!(req.start_address, 40001);
655        assert_eq!(req.quantity(), 3);
656    }
657
658    #[test]
659    fn test_write_multiple_registers_request_zero_rejected() {
660        assert!(WriteMultipleRegistersRequest::new(0, vec![]).is_err());
661    }
662
663    #[test]
664    fn test_write_multiple_registers_request_over_max_rejected() {
665        let too_many = vec![0u16; 124];
666        assert!(WriteMultipleRegistersRequest::new(0, too_many).is_err());
667    }
668
669    #[test]
670    fn test_write_multiple_registers_encode_decode() {
671        let values: Vec<u16> = (1..=5).collect();
672        let req = WriteMultipleRegistersRequest::new(100, values.clone()).expect("valid");
673        let encoded = req.encode();
674        assert_eq!(encoded[0], 0x10);
675        let decoded = WriteMultipleRegistersRequest::decode(&encoded[1..]).expect("decoded");
676        assert_eq!(decoded.values, values);
677    }
678
679    #[test]
680    fn test_write_multiple_registers_response_encode_decode() {
681        let resp = WriteMultipleRegistersResponse::new(100, 5);
682        let encoded = resp.encode();
683        assert_eq!(encoded[0], 0x10);
684        let decoded = WriteMultipleRegistersResponse::decode(&encoded[1..]).expect("decoded");
685        assert_eq!(decoded, resp);
686    }
687
688    #[test]
689    fn test_write_multiple_registers_encoding_length() {
690        // 3 registers: func(1) + start(2) + qty(2) + byte_count(1) + data(6) = 12
691        let req = WriteMultipleRegistersRequest::new(0, vec![1, 2, 3]).expect("valid");
692        assert_eq!(req.encode().len(), 12);
693    }
694
695    #[test]
696    fn test_write_multiple_registers_max_count() {
697        let values = vec![0xFFFFu16; 123];
698        assert!(WriteMultipleRegistersRequest::new(0, values).is_ok());
699    }
700}