Skip to main content

rusty_modbus_codec/request/
bit_write.rs

1//! Bit-access write requests: FC 05 (Write Single Coil) and FC 0F (Write Multiple Coils).
2
3use rusty_modbus_types::{Address, CoilValue, FunctionCode, Quantity};
4
5use crate::error::{DecodeError, EncodeError};
6use crate::request::Encode;
7
8/// FC 0x05 — Write Single Coil request.
9///
10/// Sets a single coil at `address` to `value` (ON = 0xFF00, OFF = 0x0000).
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub struct WriteSingleCoilRequest {
13    /// Coil address (0-indexed).
14    pub address: Address,
15    /// Coil value (ON or OFF).
16    pub value: CoilValue,
17}
18
19impl WriteSingleCoilRequest {
20    /// Decode from PDU data after the function code byte.
21    ///
22    /// # Errors
23    ///
24    /// Returns [`DecodeError::Truncated`] if `data` is shorter than 4 bytes.
25    /// Returns [`DecodeError::LengthMismatch`] if `data` has extra bytes.
26    /// Returns [`DecodeError::InvalidCoilValue`] if the value is not 0xFF00 or 0x0000.
27    pub fn decode(data: &[u8]) -> Result<Self, DecodeError> {
28        DecodeError::check_exact_len(data, 4)?;
29        let address = Address(u16::from_be_bytes([data[0], data[1]]));
30        let raw_value = u16::from_be_bytes([data[2], data[3]]);
31        let value =
32            CoilValue::from_wire(raw_value).ok_or(DecodeError::InvalidCoilValue(raw_value))?;
33        Ok(Self { address, value })
34    }
35}
36
37impl Encode for WriteSingleCoilRequest {
38    fn encode_into(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
39        let len = self.encoded_len();
40        if buf.len() < len {
41            return Err(EncodeError::BufferTooSmall {
42                required: len,
43                available: buf.len(),
44            });
45        }
46        EncodeError::check_pdu_len(len)?;
47        buf[0] = FunctionCode::WriteSingleCoil.code();
48        buf[1..3].copy_from_slice(&self.address.0.to_be_bytes());
49        buf[3..5].copy_from_slice(&self.value.to_wire().to_be_bytes());
50        Ok(len)
51    }
52
53    fn encoded_len(&self) -> usize {
54        5
55    }
56}
57
58/// FC 0x0F — Write Multiple Coils request.
59///
60/// Writes 1..=1968 contiguous coils starting at `address`. Coil values are
61/// packed as bits in `coil_values`, LSB of the first byte is the lowest address.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct WriteMultipleCoilsRequest<'buf> {
64    /// Starting address (0-indexed).
65    pub address: Address,
66    /// Number of coils to write (1..=1968).
67    pub quantity: Quantity,
68    /// Number of data bytes that follow.
69    pub byte_count: u8,
70    /// Packed coil values (bit-packed, LSB first).
71    pub coil_values: &'buf [u8],
72}
73
74impl<'buf> WriteMultipleCoilsRequest<'buf> {
75    /// Maximum quantity for Write Multiple Coils.
76    const MAX_QUANTITY: u16 = 1968;
77
78    /// Decode from PDU data after the function code byte.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`DecodeError::Truncated`] if `data` is too short.
83    /// Returns [`DecodeError::QuantityOutOfRange`] if the quantity is not in 1..=1968.
84    /// Returns [`DecodeError::ByteCountMismatch`] if the declared byte count does not
85    /// match the remaining data length.
86    pub fn decode(data: &'buf [u8]) -> Result<Self, DecodeError> {
87        if data.len() < 5 {
88            return Err(DecodeError::Truncated {
89                expected: 5,
90                actual: data.len(),
91            });
92        }
93        let address = Address(u16::from_be_bytes([data[0], data[1]]));
94        let quantity = u16::from_be_bytes([data[2], data[3]]);
95        if quantity == 0 || quantity > Self::MAX_QUANTITY {
96            return Err(DecodeError::QuantityOutOfRange { quantity });
97        }
98        let byte_count = data[4];
99        // Semantic check per spec Figure 21: byte_count must equal ceil(quantity/8).
100        let expected_bytes = quantity.div_ceil(8);
101        if u16::from(byte_count) != expected_bytes {
102            return Err(DecodeError::ByteCountMismatch {
103                declared: usize::from(byte_count),
104                actual: expected_bytes as usize,
105            });
106        }
107        // Wire check: declared byte_count must match actual remaining data.
108        let remaining = data.len() - 5;
109        if byte_count as usize != remaining {
110            return Err(DecodeError::ByteCountMismatch {
111                declared: byte_count as usize,
112                actual: remaining,
113            });
114        }
115        let coil_values = &data[5..];
116        Ok(Self {
117            address,
118            quantity: Quantity(quantity),
119            byte_count,
120            coil_values,
121        })
122    }
123}
124
125impl Encode for WriteMultipleCoilsRequest<'_> {
126    fn encode_into(&self, buf: &mut [u8]) -> Result<usize, EncodeError> {
127        let len = self.encoded_len();
128        if buf.len() < len {
129            return Err(EncodeError::BufferTooSmall {
130                required: len,
131                available: buf.len(),
132            });
133        }
134        EncodeError::check_quantity(self.quantity.0, Self::MAX_QUANTITY)?;
135        let expected_bytes = usize::from(self.quantity.0.div_ceil(8));
136        EncodeError::check_byte_count(usize::from(self.byte_count), expected_bytes)?;
137        EncodeError::check_byte_count(expected_bytes, self.coil_values.len())?;
138        EncodeError::check_pdu_len(len)?;
139        buf[0] = FunctionCode::WriteMultipleCoils.code();
140        buf[1..3].copy_from_slice(&self.address.0.to_be_bytes());
141        buf[3..5].copy_from_slice(&self.quantity.0.to_be_bytes());
142        buf[5] = self.byte_count;
143        buf[6..6 + self.coil_values.len()].copy_from_slice(self.coil_values);
144        Ok(len)
145    }
146
147    fn encoded_len(&self) -> usize {
148        // FC(1) + address(2) + quantity(2) + byte_count(1) + coil_values
149        6 + self.coil_values.len()
150    }
151}