Skip to main content

oms_modbus/
frame.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//!
3//! Modbus frame types — Request, Response, Exception, FunctionCode.
4
5use std::borrow::Cow;
6use std::convert::TryFrom;
7use std::io::{Error, ErrorKind};
8
9use bytes::{Buf, BufMut, Bytes, BytesMut};
10use thiserror::Error;
11
12/// Maximum PDU size per MODBUS Application Protocol V1.1b3 §4.1.
13/// The PDU is function_code (1 byte) + data (max 252 bytes) = 253 bytes.
14pub const MAX_PDU_SIZE: usize = 253;
15
16/// Maximum number of coils per request per MODBUS Application Protocol V1.1b3.
17/// Functions: ReadCoils (FC=1), WriteMultipleCoils (FC=15).
18pub const MAX_COILS: u16 = 2000;
19
20/// Maximum number of registers per request per MODBUS Application Protocol V1.1b3.
21/// Functions: ReadHoldingRegisters (FC=3), ReadInputRegisters (FC=4),
22/// WriteMultipleRegisters (FC=16), ReadWriteMultipleRegisters (FC=23).
23pub const MAX_REGISTERS: u16 = 125;
24
25/// Safe conversion from `usize` to `u8`, returning an error on overflow.
26/// Used for byte-count fields in Modbus frames where counts must fit in u8.
27#[inline]
28fn safe_u8(val: usize, context: &str) -> Result<u8, Error> {
29    u8::try_from(val).map_err(|_| {
30        Error::new(
31            ErrorKind::InvalidData,
32            format!("{context}: value {val} exceeds u8 range"),
33        )
34    })
35}
36
37/// Validate that a response's payload size will fit within the Modbus PDU limit.
38/// Called by `encode_response_into` before encoding to catch oversized payloads.
39fn validate_response_size(rsp: &Response) -> Result<(), Error> {
40    let byte_count = match rsp {
41        Response::ReadCoils(bits) | Response::ReadDiscreteInputs(bits) => bits.len().div_ceil(8),
42        Response::ReadHoldingRegisters(regs)
43        | Response::ReadInputRegisters(regs)
44        | Response::ReadWriteMultipleRegisters(regs) => regs
45            .len()
46            .checked_mul(2)
47            .ok_or_else(|| Error::new(ErrorKind::InvalidData, "response too large"))?,
48        _ => return Ok(()), // fixed-size responses always fit
49    };
50    safe_u8(byte_count, "response byte count")?;
51    Ok(())
52}
53
54// ── Function Code ─────────────────────────────────────────────────────────
55
56/// Modbus function code (1-127 for standard, 128-255 for exception responses).
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct FunctionCode(u8);
59
60impl FunctionCode {
61    #[inline]
62    pub const fn new(value: u8) -> Self {
63        Self(value)
64    }
65    #[inline]
66    pub const fn value(self) -> u8 {
67        self.0
68    }
69}
70
71/// Returns `true` if `b` is a known Modbus function code or its exception
72/// variant (standard FC + 0x80). Used by server-side frame parsers to
73/// distinguish valid frames from noise on the wire.
74///
75/// Known standard function codes: 1–6, 8, 15, 16, 22, 23.
76/// Exception variants: standard + 0x80 (bit 7 set).
77#[inline]
78pub(crate) fn is_known_function_code(b: u8) -> bool {
79    // Strip exception bit (bit 7) and check standard range.
80    let base = b & 0x7F;
81    matches!(base, 1..=6 | 8 | 15 | 16 | 22 | 23)
82}
83
84// ── Address & Quantity ────────────────────────────────────────────────────
85
86/// Modbus register/coil address (0–65535, 0-based per spec).
87pub type Address = u16;
88/// Number of registers or coils to read/write.
89pub type Quantity = u16;
90
91// ── Request ───────────────────────────────────────────────────────────────
92
93/// A Modbus request PDU — one variant per standard function code.
94///
95/// Use [`Request::function_code`] to get the numeric FC, and
96/// [`encode_request_into`] or [`TryFrom`]`<`[`Bytes`]`>` to serialize.
97#[derive(Debug, Clone, PartialEq, Eq)]
98#[non_exhaustive]
99pub enum Request<'a> {
100    ReadCoils(Address, Quantity),
101    ReadDiscreteInputs(Address, Quantity),
102    ReadHoldingRegisters(Address, Quantity),
103    ReadInputRegisters(Address, Quantity),
104    WriteSingleCoil(Address, bool),
105    WriteSingleRegister(Address, u16),
106    WriteMultipleCoils(Address, Cow<'a, [bool]>),
107    WriteMultipleRegisters(Address, Cow<'a, [u16]>),
108    ReadWriteMultipleRegisters(Address, Quantity, Address, Cow<'a, [u16]>),
109    MaskWriteRegister(Address, u16, u16),
110    /// Diagnostic (FC 08). First `u16` is sub-function code (e.g. 0x0000 =
111    /// Return Query Data, 0x000A = Clear Counters, 0x000B-0x000E = counter
112    /// reads). Second `u16` is the data field.
113    Diagnostic(u16, u16),
114    Disconnect,
115}
116
117impl Request<'_> {
118    /// The Modbus function code for this request variant.
119    pub const fn function_code(&self) -> FunctionCode {
120        use Request::*;
121        match self {
122            ReadCoils(..) => FunctionCode::new(1),
123            ReadDiscreteInputs(..) => FunctionCode::new(2),
124            ReadHoldingRegisters(..) => FunctionCode::new(3),
125            ReadInputRegisters(..) => FunctionCode::new(4),
126            WriteSingleCoil(..) => FunctionCode::new(5),
127            WriteSingleRegister(..) => FunctionCode::new(6),
128            ReadWriteMultipleRegisters(..) => FunctionCode::new(23),
129            WriteMultipleCoils(..) => FunctionCode::new(15),
130            WriteMultipleRegisters(..) => FunctionCode::new(16),
131            MaskWriteRegister(..) => FunctionCode::new(22),
132            Diagnostic(..) => FunctionCode::new(8),
133            Disconnect => FunctionCode::new(0),
134        }
135    }
136
137    /// Convert any borrowed data to `'static` owned data. Useful for storing
138    /// requests across await points or sending them to another task.
139    pub fn into_owned(self) -> Request<'static> {
140        match self {
141            Request::ReadCoils(a, q) => Request::ReadCoils(a, q),
142            Request::ReadDiscreteInputs(a, q) => Request::ReadDiscreteInputs(a, q),
143            Request::ReadHoldingRegisters(a, q) => Request::ReadHoldingRegisters(a, q),
144            Request::ReadInputRegisters(a, q) => Request::ReadInputRegisters(a, q),
145            Request::WriteSingleCoil(a, v) => Request::WriteSingleCoil(a, v),
146            Request::WriteSingleRegister(a, v) => Request::WriteSingleRegister(a, v),
147            Request::WriteMultipleCoils(a, v) => {
148                Request::WriteMultipleCoils(a, Cow::Owned(v.into_owned()))
149            }
150            Request::WriteMultipleRegisters(a, v) => {
151                Request::WriteMultipleRegisters(a, Cow::Owned(v.into_owned()))
152            }
153            Request::ReadWriteMultipleRegisters(a, q, w, d) => {
154                Request::ReadWriteMultipleRegisters(a, q, w, Cow::Owned(d.into_owned()))
155            }
156            Request::MaskWriteRegister(a, b, c) => Request::MaskWriteRegister(a, b, c),
157            Request::Diagnostic(sf, d) => Request::Diagnostic(sf, d),
158            Request::Disconnect => Request::Disconnect,
159        }
160    }
161}
162
163// ── Response ──────────────────────────────────────────────────────────────
164
165/// A Modbus response PDU — one variant per function code, plus [`Exception`].
166///
167/// Use [`Response::function_code`] to get the numeric FC (MSB set for exceptions),
168/// and [`encode_response_into`] or [`From`]`<`[`Bytes`]`>` to serialize.
169#[derive(Debug, Clone, PartialEq)]
170#[non_exhaustive]
171pub enum Response {
172    ReadCoils(Vec<bool>),
173    ReadDiscreteInputs(Vec<bool>),
174    ReadHoldingRegisters(Vec<u16>),
175    ReadInputRegisters(Vec<u16>),
176    WriteSingleCoil(Address, bool),
177    WriteSingleRegister(Address, u16),
178    WriteMultipleCoils(Address, u16),
179    WriteMultipleRegisters(Address, u16),
180    ReadWriteMultipleRegisters(Vec<u16>),
181    MaskWriteRegister(Address, u16, u16),
182    Diagnostic(u16, u16),
183    Exception(u8, Exception),
184}
185
186impl Response {
187    /// The Modbus function code for this response.
188    /// Returns `fc | 0x80` for [`Exception`](Response::Exception) variants.
189    pub const fn function_code(&self) -> FunctionCode {
190        use Response::*;
191        match self {
192            ReadCoils(..) => FunctionCode::new(1),
193            ReadDiscreteInputs(..) => FunctionCode::new(2),
194            ReadHoldingRegisters(..) => FunctionCode::new(3),
195            ReadInputRegisters(..) => FunctionCode::new(4),
196            WriteSingleCoil(..) => FunctionCode::new(5),
197            WriteSingleRegister(..) => FunctionCode::new(6),
198            WriteMultipleCoils(..) => FunctionCode::new(15),
199            WriteMultipleRegisters(..) => FunctionCode::new(16),
200            ReadWriteMultipleRegisters(..) => FunctionCode::new(23),
201            MaskWriteRegister(..) => FunctionCode::new(22),
202            Diagnostic(..) => FunctionCode::new(8),
203            Exception(fc, _) => FunctionCode::new(*fc | 0x80),
204        }
205    }
206}
207
208// ── Response → ModbusError conversion ─────────────────────────────────────
209
210impl From<Response> for crate::error::ModbusError {
211    /// Convert a `Response` to a `ModbusError`.
212    ///
213    /// - `Response::Exception` → `ModbusError::Exception`
214    /// - Everything else        → `ModbusError::Protocol` (unexpected success response
215    ///   in an error context — prefer [`unexpected_response`](crate::client)
216    ///   for direct handling in `ModbusClient` default methods)
217    fn from(rsp: Response) -> Self {
218        match rsp {
219            Response::Exception(fc, ex) => crate::error::ModbusError::exception(fc, u8::from(ex)),
220            other => crate::error::ModbusError::protocol(format!(
221                "unexpected success response in error context: {other:?}"
222            )),
223        }
224    }
225}
226
227// ── Exception ─────────────────────────────────────────────────────────────
228
229/// Standard Modbus exception codes (1–8, 10–11) plus [`Custom(u8)`](Exception::Custom).
230///
231/// Implements `From<u8>` and `Into<u8>` for wire-format conversion.
232#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
233#[repr(u8)]
234#[non_exhaustive]
235pub enum Exception {
236    #[error("Illegal function")]
237    IllegalFunction = 1,
238    #[error("Illegal data address")]
239    IllegalDataAddress = 2,
240    #[error("Illegal data value")]
241    IllegalDataValue = 3,
242    #[error("Server device failure")]
243    ServerDeviceFailure = 4,
244    #[error("Acknowledge")]
245    Acknowledge = 5,
246    #[error("Server device busy")]
247    ServerDeviceBusy = 6,
248    #[error("Negative acknowledge")]
249    NegativeAcknowledge = 7,
250    #[error("Memory parity error")]
251    MemoryParityError = 8,
252    #[error("Gateway path unavailable")]
253    GatewayPathUnavailable = 10,
254    #[error("Gateway target device failed to respond")]
255    GatewayTargetDeviceFailedToRespond = 11,
256    #[error("Custom({0})")]
257    Custom(u8),
258}
259
260impl From<u8> for Exception {
261    fn from(code: u8) -> Self {
262        match code {
263            1 => Exception::IllegalFunction,
264            2 => Exception::IllegalDataAddress,
265            3 => Exception::IllegalDataValue,
266            4 => Exception::ServerDeviceFailure,
267            5 => Exception::Acknowledge,
268            6 => Exception::ServerDeviceBusy,
269            7 => Exception::NegativeAcknowledge,
270            8 => Exception::MemoryParityError,
271            10 => Exception::GatewayPathUnavailable,
272            11 => Exception::GatewayTargetDeviceFailedToRespond,
273            n => Exception::Custom(n),
274        }
275    }
276}
277
278impl From<Exception> for u8 {
279    fn from(e: Exception) -> u8 {
280        match e {
281            Exception::IllegalFunction => 1,
282            Exception::IllegalDataAddress => 2,
283            Exception::IllegalDataValue => 3,
284            Exception::ServerDeviceFailure => 4,
285            Exception::Acknowledge => 5,
286            Exception::ServerDeviceBusy => 6,
287            Exception::NegativeAcknowledge => 7,
288            Exception::MemoryParityError => 8,
289            Exception::GatewayPathUnavailable => 10,
290            Exception::GatewayTargetDeviceFailedToRespond => 11,
291            Exception::Custom(n) => n,
292        }
293    }
294}
295
296// ── Exception Response ────────────────────────────────────────────────────
297
298/// A decoded Modbus exception response: function code and exception code.
299///
300/// Converts to [`ModbusError`](crate::ModbusError) via `From`.
301#[derive(Debug, Clone, Error)]
302#[error("Modbus exception {exception:?} for function {function:?}")]
303pub struct ExceptionResponse {
304    pub function: FunctionCode,
305    pub exception: Exception,
306}
307
308impl From<ExceptionResponse> for crate::error::ModbusError {
309    fn from(er: ExceptionResponse) -> Self {
310        crate::error::ModbusError::exception(er.function.value(), u8::from(er.exception))
311    }
312}
313
314// ── PDU Serialization ─────────────────────────────────────────────────────
315//
316// These are the ONLY conversion functions for Request ↔ Bytes and
317// Response ↔ Bytes.  Everything else goes through these.
318//
319// For zero-copy framing, transports should call `encode_request_into` /
320// `encode_response_into` directly with a reused `BytesMut` buffer instead of
321// round-tripping through `Bytes`.
322
323/// Encode a request PDU into an existing buffer (no intermediate allocation).
324///
325/// # Errors
326///
327/// Returns an error if the PDU exceeds the Modbus spec limit of 253 bytes.
328pub fn encode_request_into(req: &Request<'_>, buf: &mut BytesMut) -> Result<(), Error> {
329    let start = buf.len();
330    encode_request(req, buf)?;
331    let pdu_len = buf.len() - start;
332    if pdu_len > MAX_PDU_SIZE {
333        buf.truncate(start);
334        return Err(Error::new(
335            ErrorKind::InvalidData,
336            format!("PDU size {pdu_len} exceeds Modbus limit of {MAX_PDU_SIZE}"),
337        ));
338    }
339    Ok(())
340}
341
342/// Encode a response PDU into an existing buffer (no intermediate allocation).
343///
344/// # Errors
345///
346/// Returns an error if the response byte count overflows a `u8` or the
347/// resulting PDU exceeds the Modbus spec limit of 253 bytes.
348pub fn encode_response_into(rsp: &Response, buf: &mut BytesMut) -> Result<(), Error> {
349    // Validate byte counts before encoding to catch oversized responses early.
350    // The Modbus spec guarantees these bounds (≤2000 coils → ≤250 bytes,
351    // ≤125 registers → ≤250 bytes), but we validate here for defense in depth.
352    validate_response_size(rsp)?;
353    let start = buf.len();
354    encode_response(rsp, buf)?;
355    let pdu_len = buf.len() - start;
356    if pdu_len > MAX_PDU_SIZE {
357        buf.truncate(start);
358        return Err(Error::new(
359            ErrorKind::InvalidData,
360            format!("PDU size {pdu_len} exceeds Modbus limit of {MAX_PDU_SIZE}"),
361        ));
362    }
363    Ok(())
364}
365
366impl<'a> TryFrom<Request<'a>> for Bytes {
367    type Error = Error;
368    fn try_from(req: Request<'a>) -> Result<Self, Self::Error> {
369        let mut buf = BytesMut::new();
370        encode_request_into(&req, &mut buf)?;
371        Ok(buf.freeze())
372    }
373}
374
375impl TryFrom<Bytes> for Request<'static> {
376    type Error = Error;
377    fn try_from(mut bytes: Bytes) -> Result<Self, Self::Error> {
378        if bytes.is_empty() {
379            return Err(Error::new(ErrorKind::InvalidData, "empty PDU"));
380        }
381        let fc = bytes[0];
382        bytes.advance(1);
383        decode_request(fc, &mut bytes)
384    }
385}
386
387/// Converts a [`Response`] into its wire-format PDU bytes.
388///
389/// Uses [`encode_response_into`] internally for validation. If the
390/// response violates the Modbus spec (e.g., `>2000` coils or `>125`
391/// registers), an empty `Bytes` is returned rather than truncating or
392/// panicking. Prefer [`encode_response_into`] directly when you need
393/// error feedback.
394impl From<Response> for Bytes {
395    /// Converts a [`Response`] into its wire-format PDU bytes.
396    ///
397    /// Uses [`encode_response_into`] internally for validation. If the
398    /// response violates the Modbus spec (e.g., `>2000` coils or `>125`
399    /// registers), an empty `Bytes` is returned rather than truncating or
400    /// panicking. When you need error feedback on oversized payloads, use
401    /// [`encode_response_into`] directly — it returns `Result` so encoding
402    /// failures are surfaced rather than silently discarded.
403    fn from(rsp: Response) -> Self {
404        let mut buf = BytesMut::new();
405        let _ = encode_response_into(&rsp, &mut buf);
406        buf.freeze()
407    }
408}
409
410impl TryFrom<Bytes> for Response {
411    type Error = Error;
412    fn try_from(mut bytes: Bytes) -> Result<Self, Self::Error> {
413        if bytes.is_empty() {
414            return Err(Error::new(ErrorKind::InvalidData, "empty PDU"));
415        }
416        let fc = bytes[0];
417        // Check for exception response (function code + 0x80)
418        if fc & 0x80 != 0 {
419            if bytes.len() < 2 {
420                return Err(Error::new(
421                    ErrorKind::InvalidData,
422                    "truncated exception PDU",
423                ));
424            }
425            let exception_code = bytes[1];
426            return Ok(Response::Exception(
427                fc & 0x7f,
428                Exception::from(exception_code),
429            ));
430        }
431        bytes.advance(1);
432        decode_response(fc, &mut bytes)
433    }
434}
435
436// ── Encoders ──────────────────────────────────────────────────────────────
437
438#[inline]
439fn push_u16(buf: &mut BytesMut, v: u16) {
440    buf.put_u16(v);
441}
442
443fn encode_request(req: &Request<'_>, buf: &mut BytesMut) -> Result<(), Error> {
444    // Disconnect sends nothing on the wire
445    if matches!(req, Request::Disconnect) {
446        return Ok(());
447    }
448    buf.put_u8(req.function_code().value());
449    match req {
450        Request::ReadCoils(addr, qty)
451        | Request::ReadDiscreteInputs(addr, qty)
452        | Request::ReadHoldingRegisters(addr, qty)
453        | Request::ReadInputRegisters(addr, qty) => {
454            push_u16(buf, *addr);
455            push_u16(buf, *qty);
456        }
457        Request::WriteSingleCoil(addr, value) => {
458            push_u16(buf, *addr);
459            buf.put_u16(if *value { 0xFF00 } else { 0x0000 });
460        }
461        Request::WriteSingleRegister(addr, value) => {
462            push_u16(buf, *addr);
463            push_u16(buf, *value);
464        }
465        Request::WriteMultipleCoils(addr, values) => {
466            push_u16(buf, *addr);
467            push_u16(buf, values.len() as u16);
468            let byte_count = safe_u8(values.len().div_ceil(8), "coil byte count")?;
469            buf.put_u8(byte_count);
470            for chunk in values.chunks(8) {
471                let mut byte = 0u8;
472                for (i, &v) in chunk.iter().enumerate() {
473                    if v {
474                        byte |= 1 << i;
475                    }
476                }
477                buf.put_u8(byte);
478            }
479        }
480        Request::WriteMultipleRegisters(addr, values) => {
481            push_u16(buf, *addr);
482            push_u16(buf, values.len() as u16);
483            let byte_count = safe_u8(values.len() * 2, "write multiple reg byte count")?;
484            buf.put_u8(byte_count);
485            for &v in values.iter() {
486                push_u16(buf, v);
487            }
488        }
489        Request::ReadWriteMultipleRegisters(read_addr, read_qty, write_addr, data) => {
490            push_u16(buf, *read_addr);
491            push_u16(buf, *read_qty);
492            push_u16(buf, *write_addr);
493            push_u16(buf, data.len() as u16);
494            let byte_count = safe_u8(data.len() * 2, "read-write reg byte count")?;
495            buf.put_u8(byte_count);
496            for &v in data.iter() {
497                push_u16(buf, v);
498            }
499        }
500        Request::MaskWriteRegister(addr, and_mask, or_mask) => {
501            push_u16(buf, *addr);
502            push_u16(buf, *and_mask);
503            push_u16(buf, *or_mask);
504        }
505        Request::Diagnostic(sf, data) => {
506            push_u16(buf, *sf);
507            push_u16(buf, *data);
508        }
509        // Disconnect is a virtual request — produces no bytes on the wire.
510        // The caller (AsciiClient::send_recv) checks for it before calling encode.
511        Request::Disconnect => {}
512    }
513    Ok(())
514}
515
516fn decode_request(fc: u8, data: &mut Bytes) -> Result<Request<'static>, Error> {
517    // Macro to read u16 with bounds check — never panics on malformed data
518    macro_rules! read_u16 {
519        ($data:expr) => {{
520            if $data.remaining() < 2 {
521                return Err(Error::new(ErrorKind::UnexpectedEof, "truncated PDU"));
522            }
523            $data.get_u16()
524        }};
525    }
526    macro_rules! read_u8 {
527        ($data:expr) => {{
528            if $data.remaining() < 1 {
529                return Err(Error::new(ErrorKind::UnexpectedEof, "truncated PDU"));
530            }
531            $data.get_u8()
532        }};
533    }
534
535    Ok(match fc {
536        1 => {
537            let addr = read_u16!(data);
538            let qty = read_u16!(data);
539            if qty == 0 || qty > MAX_COILS {
540                return Err(Error::new(
541                    ErrorKind::InvalidData,
542                    format!("ReadCoils: qty {qty} not in 1..={MAX_COILS}"),
543                ));
544            }
545            if addr as u32 + qty as u32 > 0x10000 {
546                return Err(Error::new(
547                    ErrorKind::InvalidData,
548                    format!("ReadCoils: addr {addr} + qty {qty} exceeds 0xFFFF"),
549                ));
550            }
551            Request::ReadCoils(addr, qty)
552        }
553        2 => {
554            let addr = read_u16!(data);
555            let qty = read_u16!(data);
556            if qty == 0 || qty > MAX_COILS {
557                return Err(Error::new(
558                    ErrorKind::InvalidData,
559                    format!("ReadDiscreteInputs: qty {qty} not in 1..={MAX_COILS}"),
560                ));
561            }
562            if addr as u32 + qty as u32 > 0x10000 {
563                return Err(Error::new(
564                    ErrorKind::InvalidData,
565                    format!("ReadDiscreteInputs: addr {addr} + qty {qty} exceeds 0xFFFF"),
566                ));
567            }
568            Request::ReadDiscreteInputs(addr, qty)
569        }
570        3 => {
571            let addr = read_u16!(data);
572            let qty = read_u16!(data);
573            if qty == 0 || qty > MAX_REGISTERS {
574                return Err(Error::new(
575                    ErrorKind::InvalidData,
576                    format!("ReadHoldingRegisters: qty {qty} not in 1..={MAX_REGISTERS}"),
577                ));
578            }
579            if addr as u32 + qty as u32 > 0x10000 {
580                return Err(Error::new(
581                    ErrorKind::InvalidData,
582                    format!("ReadHoldingRegisters: addr {addr} + qty {qty} exceeds 0xFFFF"),
583                ));
584            }
585            Request::ReadHoldingRegisters(addr, qty)
586        }
587        4 => {
588            let addr = read_u16!(data);
589            let qty = read_u16!(data);
590            if qty == 0 || qty > MAX_REGISTERS {
591                return Err(Error::new(
592                    ErrorKind::InvalidData,
593                    format!("ReadInputRegisters: qty {qty} not in 1..={MAX_REGISTERS}"),
594                ));
595            }
596            if addr as u32 + qty as u32 > 0x10000 {
597                return Err(Error::new(
598                    ErrorKind::InvalidData,
599                    format!("ReadInputRegisters: addr {addr} + qty {qty} exceeds 0xFFFF"),
600                ));
601            }
602            Request::ReadInputRegisters(addr, qty)
603        }
604        5 => {
605            let addr = read_u16!(data);
606            let raw = read_u16!(data);
607            let val = match raw {
608                0xFF00 => true,
609                0x0000 => false,
610                other => return Err(Error::new(ErrorKind::InvalidData,
611                    format!("WriteSingleCoil: invalid coil value {other:#06X}, expected 0xFF00 or 0x0000"))),
612            };
613            Request::WriteSingleCoil(addr, val)
614        }
615        6 => {
616            let addr = read_u16!(data);
617            let val = read_u16!(data);
618            Request::WriteSingleRegister(addr, val)
619        }
620        15 => {
621            let addr = read_u16!(data);
622            let qty = read_u16!(data);
623            if qty == 0 || qty > MAX_COILS {
624                return Err(Error::new(
625                    ErrorKind::InvalidData,
626                    format!("WriteMultipleCoils: qty {qty} not in 1..={MAX_COILS}"),
627                ));
628            }
629            if addr as u32 + qty as u32 > 0x10000 {
630                return Err(Error::new(
631                    ErrorKind::InvalidData,
632                    format!("WriteMultipleCoils: addr {addr} + qty {qty} exceeds 0xFFFF"),
633                ));
634            }
635            let qty = qty as usize;
636            let byte_count = read_u8!(data) as usize;
637            let expected_byte_count = qty.div_ceil(8);
638            if byte_count != expected_byte_count {
639                return Err(Error::new(ErrorKind::InvalidData,
640                    format!("WriteMultipleCoils: byte_count {byte_count} != ceil(qty/8) ({expected_byte_count})")));
641            }
642            if data.remaining() < byte_count {
643                return Err(Error::new(ErrorKind::UnexpectedEof, "truncated coil data"));
644            }
645            let mut values = Vec::with_capacity(byte_count * 8);
646            for _ in 0..byte_count {
647                let byte = data.get_u8();
648                for i in 0..8 {
649                    values.push(byte & (1 << i) != 0);
650                    if values.len() >= qty {
651                        break;
652                    }
653                }
654            }
655            values.truncate(qty);
656            Request::WriteMultipleCoils(addr, Cow::Owned(values))
657        }
658        16 => {
659            let addr = read_u16!(data);
660            let qty = read_u16!(data);
661            if qty == 0 || qty > MAX_REGISTERS {
662                return Err(Error::new(
663                    ErrorKind::InvalidData,
664                    format!("WriteMultipleRegisters: qty {qty} not in 1..={MAX_REGISTERS}"),
665                ));
666            }
667            if addr as u32 + qty as u32 > 0x10000 {
668                return Err(Error::new(
669                    ErrorKind::InvalidData,
670                    format!("WriteMultipleRegisters: addr {addr} + qty {qty} exceeds 0xFFFF"),
671                ));
672            }
673            let qty = qty as usize;
674            let byte_count = read_u8!(data) as usize;
675            if byte_count != qty * 2 {
676                return Err(Error::new(
677                    ErrorKind::InvalidData,
678                    format!(
679                        "WriteMultipleRegisters: byte_count {byte_count} != qty*2 ({})",
680                        qty * 2
681                    ),
682                ));
683            }
684            if data.remaining() < qty * 2 {
685                return Err(Error::new(
686                    ErrorKind::UnexpectedEof,
687                    "truncated register data",
688                ));
689            }
690            let mut values = Vec::with_capacity(qty);
691            for _ in 0..qty {
692                values.push(data.get_u16());
693            }
694            Request::WriteMultipleRegisters(addr, Cow::Owned(values))
695        }
696        22 => {
697            let addr = read_u16!(data);
698            let and_mask = read_u16!(data);
699            let or_mask = read_u16!(data);
700            Request::MaskWriteRegister(addr, and_mask, or_mask)
701        }
702        23 => {
703            let read_addr = read_u16!(data);
704            let read_qty = read_u16!(data);
705            if read_qty == 0 || read_qty > MAX_REGISTERS {
706                return Err(Error::new(ErrorKind::InvalidData,
707                    format!("ReadWriteMultipleRegisters: read_qty {read_qty} not in 1..={MAX_REGISTERS}")));
708            }
709            if read_addr as u32 + read_qty as u32 > 0x10000 {
710                return Err(Error::new(ErrorKind::InvalidData,
711                    format!("ReadWriteMultipleRegisters: read_addr {read_addr} + read_qty {read_qty} exceeds 0xFFFF")));
712            }
713            let write_addr = read_u16!(data);
714            let write_qty = read_u16!(data);
715            if write_qty == 0 || write_qty > MAX_REGISTERS {
716                return Err(Error::new(ErrorKind::InvalidData,
717                    format!("ReadWriteMultipleRegisters: write_qty {write_qty} not in 1..={MAX_REGISTERS}")));
718            }
719            if write_addr as u32 + write_qty as u32 > 0x10000 {
720                return Err(Error::new(ErrorKind::InvalidData,
721                    format!("ReadWriteMultipleRegisters: write_addr {write_addr} + write_qty {write_qty} exceeds 0xFFFF")));
722            }
723            let write_qty = write_qty as usize;
724            let byte_count = read_u8!(data) as usize;
725            if byte_count != write_qty * 2 {
726                return Err(Error::new(
727                    ErrorKind::InvalidData,
728                    format!(
729                        "ReadWriteMultipleRegisters: byte_count {byte_count} != write_qty*2 ({})",
730                        write_qty * 2
731                    ),
732                ));
733            }
734            if data.remaining() < write_qty * 2 {
735                return Err(Error::new(
736                    ErrorKind::UnexpectedEof,
737                    "truncated R/W register data",
738                ));
739            }
740            let mut values = Vec::with_capacity(write_qty);
741            for _ in 0..write_qty {
742                values.push(data.get_u16());
743            }
744            Request::ReadWriteMultipleRegisters(read_addr, read_qty, write_addr, Cow::Owned(values))
745        }
746        8 => {
747            let sf = read_u16!(data);
748            let d = read_u16!(data);
749            Request::Diagnostic(sf, d)
750        }
751        _ => {
752            return Err(Error::new(
753                ErrorKind::InvalidData,
754                format!("unknown function code: {fc:#04X}"),
755            ))
756        }
757    })
758}
759
760fn encode_response(rsp: &Response, buf: &mut BytesMut) -> Result<(), Error> {
761    match rsp {
762        Response::ReadCoils(bits) | Response::ReadDiscreteInputs(bits) => {
763            let byte_count = safe_u8(bits.len().div_ceil(8), "coil byte count")?;
764            buf.put_u8(rsp.function_code().value());
765            buf.put_u8(byte_count);
766            for chunk in bits.chunks(8) {
767                let mut byte = 0u8;
768                for (i, &v) in chunk.iter().enumerate() {
769                    if v {
770                        byte |= 1 << i;
771                    }
772                }
773                buf.put_u8(byte);
774            }
775        }
776        Response::ReadHoldingRegisters(regs) | Response::ReadInputRegisters(regs) => {
777            buf.put_u8(rsp.function_code().value());
778            buf.put_u8(safe_u8(regs.len() * 2, "register byte count")?);
779            for &v in regs {
780                push_u16(buf, v);
781            }
782        }
783        Response::ReadWriteMultipleRegisters(regs) => {
784            buf.put_u8(rsp.function_code().value());
785            buf.put_u8(safe_u8(regs.len() * 2, "register byte count")?);
786            for &v in regs {
787                push_u16(buf, v);
788            }
789        }
790        Response::WriteSingleCoil(addr, val) => {
791            buf.put_u8(rsp.function_code().value());
792            push_u16(buf, *addr);
793            buf.put_u16(if *val { 0xFF00 } else { 0x0000 });
794        }
795        Response::WriteSingleRegister(addr, val) => {
796            buf.put_u8(rsp.function_code().value());
797            push_u16(buf, *addr);
798            push_u16(buf, *val);
799        }
800        Response::WriteMultipleCoils(addr, qty) => {
801            buf.put_u8(rsp.function_code().value());
802            push_u16(buf, *addr);
803            push_u16(buf, *qty);
804        }
805        Response::WriteMultipleRegisters(addr, qty) => {
806            buf.put_u8(rsp.function_code().value());
807            push_u16(buf, *addr);
808            push_u16(buf, *qty);
809        }
810        Response::MaskWriteRegister(addr, and_mask, or_mask) => {
811            buf.put_u8(rsp.function_code().value());
812            push_u16(buf, *addr);
813            push_u16(buf, *and_mask);
814            push_u16(buf, *or_mask);
815        }
816        Response::Diagnostic(sf, data) => {
817            buf.put_u8(rsp.function_code().value());
818            push_u16(buf, *sf);
819            push_u16(buf, *data);
820        }
821        Response::Exception(fc, exception) => {
822            buf.put_u8(*fc | 0x80);
823            buf.put_u8(u8::from(*exception));
824        }
825    }
826    Ok(())
827}
828
829fn decode_response(fc: u8, data: &mut Bytes) -> Result<Response, Error> {
830    macro_rules! read_u16 {
831        ($data:expr) => {{
832            if $data.remaining() < 2 {
833                return Err(Error::new(
834                    ErrorKind::UnexpectedEof,
835                    "truncated response PDU",
836                ));
837            }
838            $data.get_u16()
839        }};
840    }
841    macro_rules! read_u8 {
842        ($data:expr) => {{
843            if $data.remaining() < 1 {
844                return Err(Error::new(
845                    ErrorKind::UnexpectedEof,
846                    "truncated response PDU",
847                ));
848            }
849            $data.get_u8()
850        }};
851    }
852
853    Ok(match fc {
854        1 | 2 => {
855            let byte_count = read_u8!(data) as usize;
856            if data.remaining() < byte_count {
857                return Err(Error::new(
858                    ErrorKind::UnexpectedEof,
859                    "truncated coil response",
860                ));
861            }
862            let mut bits = Vec::with_capacity(byte_count * 8);
863            for _ in 0..byte_count {
864                let byte = data.get_u8();
865                for i in 0..8 {
866                    bits.push(byte & (1 << i) != 0);
867                }
868            }
869            if fc == 1 {
870                Response::ReadCoils(bits)
871            } else {
872                Response::ReadDiscreteInputs(bits)
873            }
874        }
875        3 | 4 => {
876            let byte_count = read_u8!(data) as usize;
877            if byte_count % 2 != 0 {
878                return Err(Error::new(
879                    ErrorKind::InvalidData,
880                    format!("register response: byte_count {byte_count} is not even"),
881                ));
882            }
883            if data.remaining() < byte_count {
884                return Err(Error::new(
885                    ErrorKind::UnexpectedEof,
886                    "truncated register response",
887                ));
888            }
889            let mut regs = Vec::with_capacity(byte_count / 2);
890            for _ in 0..(byte_count / 2) {
891                regs.push(data.get_u16());
892            }
893            if fc == 3 {
894                Response::ReadHoldingRegisters(regs)
895            } else {
896                Response::ReadInputRegisters(regs)
897            }
898        }
899        5 => {
900            let addr = read_u16!(data);
901            let val = read_u16!(data) == 0xFF00;
902            Response::WriteSingleCoil(addr, val)
903        }
904        6 => {
905            let addr = read_u16!(data);
906            let val = read_u16!(data);
907            Response::WriteSingleRegister(addr, val)
908        }
909        15 => {
910            let addr = read_u16!(data);
911            let qty = read_u16!(data);
912            Response::WriteMultipleCoils(addr, qty)
913        }
914        16 => {
915            let addr = read_u16!(data);
916            let qty = read_u16!(data);
917            Response::WriteMultipleRegisters(addr, qty)
918        }
919        22 => {
920            let addr = read_u16!(data);
921            let and_mask = read_u16!(data);
922            let or_mask = read_u16!(data);
923            Response::MaskWriteRegister(addr, and_mask, or_mask)
924        }
925        23 => {
926            let byte_count = read_u8!(data) as usize;
927            if byte_count % 2 != 0 {
928                return Err(Error::new(
929                    ErrorKind::InvalidData,
930                    format!(
931                        "ReadWriteMultipleRegisters response: byte_count {byte_count} is not even"
932                    ),
933                ));
934            }
935            if data.remaining() < byte_count {
936                return Err(Error::new(
937                    ErrorKind::UnexpectedEof,
938                    "truncated R/W response",
939                ));
940            }
941            let mut regs = Vec::with_capacity(byte_count / 2);
942            for _ in 0..(byte_count / 2) {
943                regs.push(data.get_u16());
944            }
945            Response::ReadWriteMultipleRegisters(regs)
946        }
947        8 => {
948            let sf = read_u16!(data);
949            let d = read_u16!(data);
950            Response::Diagnostic(sf, d)
951        }
952        _ => {
953            return Err(Error::new(
954                ErrorKind::InvalidData,
955                format!("unknown response function code: {fc}"),
956            ))
957        }
958    })
959}
960
961// ── Tests ────────────────────────────────────────────────────────────
962
963#[cfg(test)]
964mod tests {
965    use super::*;
966
967    #[test]
968    fn known_function_codes_accepted() {
969        // Standard FCs: 1-6, 8, 15, 16, 22, 23
970        for fc in [1u8, 2, 3, 4, 5, 6, 8, 15, 16, 22, 23] {
971            assert!(
972                is_known_function_code(fc),
973                "standard FC {fc} should be known"
974            );
975        }
976        // Exception variants: standard + 0x80
977        for fc in [
978            0x81u8, 0x82, 0x83, 0x84, 0x85, 0x86, 0x88, 0x8F, 0x90, 0x96, 0x97,
979        ] {
980            assert!(
981                is_known_function_code(fc),
982                "exception FC 0x{fc:02X} should be known"
983            );
984        }
985    }
986
987    #[test]
988    fn unknown_function_codes_rejected() {
989        for fc in [0u8, 7, 9, 14, 18, 24, 0x80, 0x87, 0xFF] {
990            assert!(
991                !is_known_function_code(fc),
992                "unknown FC {fc} should NOT be known"
993            );
994        }
995    }
996}