Skip to main content

xy_modbus/framing/
mod.rs

1//! Modbus-RTU on-wire framing — pure functions, no I/O.
2//!
3//! Use these to build a [`crate::ModbusTransport`] over your platform's
4//! UART. The codec is general Modbus-RTU (function codes `0x03`, `0x06`,
5//! `0x10`); nothing in here is XY-specific.
6//!
7//! CRC-16 is the standard reflected polynomial `0xA001`, seeded
8//! `0xFFFF`, no final XOR. The CRC is appended low-byte first.
9
10use crate::transport::ModbusError;
11
12// ─── Constants ───────────────────────────────────────────────────────────────
13
14pub(crate) const FN_READ_HOLDING: u8 = 0x03;
15pub(crate) const FN_WRITE_SINGLE: u8 = 0x06;
16pub(crate) const FN_WRITE_MULTIPLE: u8 = 0x10;
17
18/// Modbus exception flag (high bit of the function-code byte). Per the
19/// spec, function codes occupy 1..=127 — any FC byte with bit 7 set is
20/// an exception response.
21pub const EXCEPTION_BIT: u8 = 0x80;
22
23/// Maximum Modbus-RTU ADU size (slave + PDU + CRC).
24pub const MAX_ADU: usize = 256;
25
26/// Maximum registers in a single `Read Holding Registers` request
27/// (Modbus standard limit).
28pub const MAX_READ_REGS: usize = 125;
29
30/// Maximum registers in a single `Write Multiple Holdings` request
31/// (Modbus standard limit).
32pub const MAX_WRITE_REGS: usize = 123;
33
34// ─── FrameError ──────────────────────────────────────────────────────────────
35
36/// Why [`build_write_multiple_request`] could not assemble a frame.
37#[derive(Copy, Clone, Debug, PartialEq, Eq)]
38#[cfg_attr(feature = "defmt", derive(defmt::Format))]
39pub enum FrameError {
40    /// `values` was empty or exceeded [`MAX_WRITE_REGS`] (123).
41    InvalidLength(usize),
42    /// `out` was smaller than the assembled frame (header + payload + CRC).
43    BufferTooSmall { needed: usize, actual: usize },
44}
45
46impl core::fmt::Display for FrameError {
47    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
48        match self {
49            Self::InvalidLength(n) => write!(f, "invalid register count {n}"),
50            Self::BufferTooSmall { needed, actual } => {
51                write!(f, "buffer too small (need {needed}, have {actual})")
52            }
53        }
54    }
55}
56
57impl core::error::Error for FrameError {}
58
59// ─── CRC-16 ──────────────────────────────────────────────────────────────────
60
61/// Standard Modbus-RTU CRC-16.
62pub fn crc16_modbus(data: &[u8]) -> u16 {
63    let mut crc: u16 = 0xFFFF;
64    for &b in data {
65        crc ^= b as u16;
66        for _ in 0..8 {
67            if crc & 1 != 0 {
68                crc = (crc >> 1) ^ 0xA001;
69            } else {
70                crc >>= 1;
71            }
72        }
73    }
74    crc
75}
76
77fn append_crc(buf: &mut [u8], len: usize) {
78    let crc = crc16_modbus(&buf[..len]);
79    buf[len] = crc as u8;
80    buf[len + 1] = (crc >> 8) as u8;
81}
82
83// ─── Request builders ────────────────────────────────────────────────────────
84
85/// Build a `Read Holding Registers` (FC `0x03`) request frame.
86pub fn build_read_request(slave: u8, addr: u16, count: u16) -> [u8; 8] {
87    let mut req = [0u8; 8];
88    req[0] = slave;
89    req[1] = FN_READ_HOLDING;
90    req[2..4].copy_from_slice(&addr.to_be_bytes());
91    req[4..6].copy_from_slice(&count.to_be_bytes());
92    append_crc(&mut req, 6);
93    req
94}
95
96/// Build a `Write Single Holding Register` (FC `0x06`) request frame.
97pub fn build_write_single_request(slave: u8, addr: u16, value: u16) -> [u8; 8] {
98    let mut req = [0u8; 8];
99    req[0] = slave;
100    req[1] = FN_WRITE_SINGLE;
101    req[2..4].copy_from_slice(&addr.to_be_bytes());
102    req[4..6].copy_from_slice(&value.to_be_bytes());
103    append_crc(&mut req, 6);
104    req
105}
106
107/// Build a `Write Multiple Holding Registers` (FC `0x10`) request into
108/// `out`, returning the number of bytes written. `out` must be at
109/// least `9 + 2 * values.len()` bytes.
110pub fn build_write_multiple_request(
111    slave: u8,
112    addr: u16,
113    values: &[u16],
114    out: &mut [u8],
115) -> Result<usize, FrameError> {
116    if values.is_empty() || values.len() > MAX_WRITE_REGS {
117        return Err(FrameError::InvalidLength(values.len()));
118    }
119    let bc = 2 * values.len();
120    let len = 7 + bc + 2;
121    if out.len() < len {
122        return Err(FrameError::BufferTooSmall {
123            needed: len,
124            actual: out.len(),
125        });
126    }
127    out[0] = slave;
128    out[1] = FN_WRITE_MULTIPLE;
129    out[2..4].copy_from_slice(&addr.to_be_bytes());
130    out[4..6].copy_from_slice(&(values.len() as u16).to_be_bytes());
131    out[6] = bc as u8;
132    for (i, v) in values.iter().enumerate() {
133        out[7 + 2 * i..9 + 2 * i].copy_from_slice(&v.to_be_bytes());
134    }
135    append_crc(out, 7 + bc);
136    Ok(len)
137}
138
139// ─── Response parsers ────────────────────────────────────────────────────────
140
141fn check_crc(resp: &[u8], len: usize) -> Result<(), ModbusError> {
142    if resp.len() < len {
143        return Err(ModbusError::ShortResponse(resp.len()));
144    }
145    let got = u16::from_le_bytes([resp[len - 2], resp[len - 1]]);
146    let calc = crc16_modbus(&resp[..len - 2]);
147    if got == calc {
148        Ok(())
149    } else {
150        Err(ModbusError::BadCrc)
151    }
152}
153
154fn check_exception(resp: &[u8], slave: u8) -> Result<(), ModbusError> {
155    if resp.len() < 5 {
156        return Err(ModbusError::ShortResponse(resp.len()));
157    }
158    if resp[0] != slave {
159        return Err(ModbusError::BadSlave(resp[0]));
160    }
161    if resp[1] & EXCEPTION_BIT != 0 {
162        check_crc(resp, 5)?;
163        return Err(ModbusError::Exception(resp[2]));
164    }
165    Ok(())
166}
167
168/// Parse a `Read Holding Registers` response into `out`. The expected
169/// register count is `out.len()`.
170pub fn parse_read_response(resp: &[u8], slave: u8, out: &mut [u16]) -> Result<(), ModbusError> {
171    check_exception(resp, slave)?;
172    let count = out.len();
173    let expected_len = 5 + 2 * count;
174    if resp[1] != FN_READ_HOLDING || resp[2] as usize != 2 * count {
175        return Err(ModbusError::BadHeader);
176    }
177    check_crc(resp, expected_len)?;
178    for (i, slot) in out.iter_mut().enumerate() {
179        *slot = u16::from_be_bytes([resp[3 + 2 * i], resp[4 + 2 * i]]);
180    }
181    Ok(())
182}
183
184/// Parse a `Write Single Holding Register` response. Per Modbus spec the
185/// response echoes the request byte-for-byte.
186pub fn parse_write_single_response(resp: &[u8], req: &[u8; 8]) -> Result<(), ModbusError> {
187    check_exception(resp, req[0])?;
188    check_crc(resp, 8)?;
189    if resp.len() < 8 || resp[..8] != req[..] {
190        return Err(ModbusError::BadHeader);
191    }
192    Ok(())
193}
194
195/// Parse a `Write Multiple Holding Registers` response. The response is
196/// always 8 bytes: slave, fc, start addr, qty, CRC.
197pub fn parse_write_multiple_response(
198    resp: &[u8],
199    slave: u8,
200    addr: u16,
201    qty: u16,
202) -> Result<(), ModbusError> {
203    check_exception(resp, slave)?;
204    check_crc(resp, 8)?;
205    if resp[1] != FN_WRITE_MULTIPLE
206        || u16::from_be_bytes([resp[2], resp[3]]) != addr
207        || u16::from_be_bytes([resp[4], resp[5]]) != qty
208    {
209        return Err(ModbusError::BadHeader);
210    }
211    Ok(())
212}
213
214#[cfg(test)]
215mod tests;