Skip to main content

mabi_modbus/
core.rs

1//! Protocol-level Modbus core types and semantic request parsing.
2
3use crate::error::{ModbusError, ModbusResult};
4pub use crate::handler::{build_exception_pdu, ExceptionCode, ExceptionResponse};
5
6/// Supported Modbus function codes handled by the simulator core.
7#[repr(u8)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9pub enum FunctionCode {
10    ReadCoils = 0x01,
11    ReadDiscreteInputs = 0x02,
12    ReadHoldingRegisters = 0x03,
13    ReadInputRegisters = 0x04,
14    WriteSingleCoil = 0x05,
15    WriteSingleRegister = 0x06,
16    WriteMultipleCoils = 0x0F,
17    WriteMultipleRegisters = 0x10,
18    MaskWriteRegister = 0x16,
19    ReadWriteMultipleRegisters = 0x17,
20}
21
22impl TryFrom<u8> for FunctionCode {
23    type Error = ModbusError;
24
25    fn try_from(value: u8) -> Result<Self, Self::Error> {
26        match value {
27            0x01 => Ok(Self::ReadCoils),
28            0x02 => Ok(Self::ReadDiscreteInputs),
29            0x03 => Ok(Self::ReadHoldingRegisters),
30            0x04 => Ok(Self::ReadInputRegisters),
31            0x05 => Ok(Self::WriteSingleCoil),
32            0x06 => Ok(Self::WriteSingleRegister),
33            0x0F => Ok(Self::WriteMultipleCoils),
34            0x10 => Ok(Self::WriteMultipleRegisters),
35            0x16 => Ok(Self::MaskWriteRegister),
36            0x17 => Ok(Self::ReadWriteMultipleRegisters),
37            _ => Err(ModbusError::InvalidFunction(value)),
38        }
39    }
40}
41
42impl From<FunctionCode> for u8 {
43    fn from(value: FunctionCode) -> Self {
44        value as u8
45    }
46}
47
48/// A validated Modbus Protocol Data Unit request.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct RequestPdu {
51    bytes: Vec<u8>,
52}
53
54impl RequestPdu {
55    /// Create a validated request PDU.
56    pub fn new(bytes: impl Into<Vec<u8>>) -> ModbusResult<Self> {
57        let bytes = bytes.into();
58        if bytes.is_empty() {
59            return Err(ModbusError::InvalidData(
60                "request PDU must contain at least a function code".to_string(),
61            ));
62        }
63        Ok(Self { bytes })
64    }
65
66    /// Borrow the raw PDU bytes.
67    pub fn as_bytes(&self) -> &[u8] {
68        &self.bytes
69    }
70
71    /// Consume the PDU into raw bytes.
72    pub fn into_bytes(self) -> Vec<u8> {
73        self.bytes
74    }
75
76    /// Read the raw function code byte.
77    pub fn raw_function_code(&self) -> u8 {
78        self.bytes[0]
79    }
80
81    /// Parse the function code.
82    pub fn function_code(&self) -> ModbusResult<FunctionCode> {
83        FunctionCode::try_from(self.raw_function_code())
84    }
85
86    /// Parse the PDU into the canonical typed semantic model.
87    pub fn semantic_request(&self, is_broadcast: bool) -> Result<SemanticRequest, ExceptionCode> {
88        parse_semantic_request(self.as_bytes(), is_broadcast)
89    }
90}
91
92/// A validated Modbus response PDU.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ResponsePdu {
95    bytes: Vec<u8>,
96}
97
98impl ResponsePdu {
99    /// Create a validated response PDU.
100    pub fn new(bytes: impl Into<Vec<u8>>) -> ModbusResult<Self> {
101        let bytes = bytes.into();
102        if bytes.is_empty() {
103            return Err(ModbusError::InvalidData(
104                "response PDU must contain at least a function code".to_string(),
105            ));
106        }
107        Ok(Self { bytes })
108    }
109
110    /// Borrow the raw PDU bytes.
111    pub fn as_bytes(&self) -> &[u8] {
112        &self.bytes
113    }
114
115    /// Consume the PDU into raw bytes.
116    pub fn into_bytes(self) -> Vec<u8> {
117        self.bytes
118    }
119
120    /// Check whether the response is an exception response.
121    pub fn is_exception(&self) -> bool {
122        ExceptionResponse::is_exception(self.as_bytes())
123    }
124}
125
126/// Canonical typed semantic request model used by the built-in core.
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub enum SemanticRequest {
129    ReadCoils {
130        address: u16,
131        quantity: u16,
132    },
133    ReadDiscreteInputs {
134        address: u16,
135        quantity: u16,
136    },
137    ReadHoldingRegisters {
138        address: u16,
139        quantity: u16,
140    },
141    ReadInputRegisters {
142        address: u16,
143        quantity: u16,
144    },
145    WriteSingleCoil {
146        address: u16,
147        value: bool,
148    },
149    WriteSingleRegister {
150        address: u16,
151        value: u16,
152    },
153    WriteMultipleCoils {
154        address: u16,
155        values: Vec<bool>,
156    },
157    WriteMultipleRegisters {
158        address: u16,
159        values: Vec<u16>,
160    },
161    MaskWriteRegister {
162        address: u16,
163        and_mask: u16,
164        or_mask: u16,
165    },
166    ReadWriteMultipleRegisters {
167        read_address: u16,
168        read_quantity: u16,
169        write_address: u16,
170        values: Vec<u16>,
171    },
172    Custom {
173        function_code: u8,
174        payload: Vec<u8>,
175    },
176}
177
178impl SemanticRequest {
179    /// Returns the function code for the request.
180    pub fn function_code(&self) -> u8 {
181        match self {
182            Self::ReadCoils { .. } => FunctionCode::ReadCoils as u8,
183            Self::ReadDiscreteInputs { .. } => FunctionCode::ReadDiscreteInputs as u8,
184            Self::ReadHoldingRegisters { .. } => FunctionCode::ReadHoldingRegisters as u8,
185            Self::ReadInputRegisters { .. } => FunctionCode::ReadInputRegisters as u8,
186            Self::WriteSingleCoil { .. } => FunctionCode::WriteSingleCoil as u8,
187            Self::WriteSingleRegister { .. } => FunctionCode::WriteSingleRegister as u8,
188            Self::WriteMultipleCoils { .. } => FunctionCode::WriteMultipleCoils as u8,
189            Self::WriteMultipleRegisters { .. } => FunctionCode::WriteMultipleRegisters as u8,
190            Self::MaskWriteRegister { .. } => FunctionCode::MaskWriteRegister as u8,
191            Self::ReadWriteMultipleRegisters { .. } => {
192                FunctionCode::ReadWriteMultipleRegisters as u8
193            }
194            Self::Custom { function_code, .. } => *function_code,
195        }
196    }
197
198    /// Whether the function is broadcast-safe according to the Modbus spec.
199    pub fn allows_broadcast(&self) -> bool {
200        matches!(
201            self,
202            Self::WriteSingleCoil { .. }
203                | Self::WriteSingleRegister { .. }
204                | Self::WriteMultipleCoils { .. }
205                | Self::WriteMultipleRegisters { .. }
206                | Self::MaskWriteRegister { .. }
207        )
208    }
209}
210
211/// Canonical typed semantic response model used by the built-in core.
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub enum SemanticResponse {
214    Bits {
215        function: FunctionCode,
216        values: Vec<bool>,
217    },
218    Registers {
219        function: FunctionCode,
220        values: Vec<u16>,
221    },
222    WriteSingleCoilAck {
223        address: u16,
224        value: bool,
225    },
226    WriteSingleRegisterAck {
227        address: u16,
228        value: u16,
229    },
230    WriteMultipleAck {
231        function: FunctionCode,
232        address: u16,
233        quantity: u16,
234    },
235    MaskWriteAck {
236        address: u16,
237        and_mask: u16,
238        or_mask: u16,
239    },
240    Custom(ResponsePdu),
241}
242
243/// Parse raw PDU bytes into the canonical typed semantic model.
244pub fn parse_semantic_request(
245    pdu: &[u8],
246    is_broadcast: bool,
247) -> Result<SemanticRequest, ExceptionCode> {
248    let function_code = *pdu.first().ok_or(ExceptionCode::IllegalDataValue)?;
249    let request = match FunctionCode::try_from(function_code) {
250        Ok(FunctionCode::ReadCoils) => parse_read_bits(FunctionCode::ReadCoils, pdu)?,
251        Ok(FunctionCode::ReadDiscreteInputs) => {
252            parse_read_bits(FunctionCode::ReadDiscreteInputs, pdu)?
253        }
254        Ok(FunctionCode::ReadHoldingRegisters) => {
255            parse_read_registers(FunctionCode::ReadHoldingRegisters, pdu)?
256        }
257        Ok(FunctionCode::ReadInputRegisters) => {
258            parse_read_registers(FunctionCode::ReadInputRegisters, pdu)?
259        }
260        Ok(FunctionCode::WriteSingleCoil) => parse_write_single_coil(pdu)?,
261        Ok(FunctionCode::WriteSingleRegister) => parse_write_single_register(pdu)?,
262        Ok(FunctionCode::WriteMultipleCoils) => parse_write_multiple_coils(pdu)?,
263        Ok(FunctionCode::WriteMultipleRegisters) => parse_write_multiple_registers(pdu)?,
264        Ok(FunctionCode::MaskWriteRegister) => parse_mask_write_register(pdu)?,
265        Ok(FunctionCode::ReadWriteMultipleRegisters) => parse_read_write_multiple_registers(pdu)?,
266        Err(ModbusError::InvalidFunction(_)) => SemanticRequest::Custom {
267            function_code,
268            payload: pdu[1..].to_vec(),
269        },
270        Err(_) => return Err(ExceptionCode::IllegalFunction),
271    };
272
273    if is_broadcast && !request.allows_broadcast() {
274        return Err(ExceptionCode::IllegalFunction);
275    }
276
277    Ok(request)
278}
279
280impl SemanticResponse {
281    /// Encode the typed response back into a wire PDU.
282    pub fn encode(self) -> ModbusResult<ResponsePdu> {
283        let bytes = match self {
284            Self::Bits { function, values } => {
285                let mut response = vec![function as u8, values.len().div_ceil(8) as u8];
286                response.extend(pack_bits(&values));
287                response
288            }
289            Self::Registers { function, values } => {
290                let mut response = vec![function as u8, (values.len() * 2) as u8];
291                for value in values {
292                    response.extend_from_slice(&value.to_be_bytes());
293                }
294                response
295            }
296            Self::WriteSingleCoilAck { address, value } => {
297                let mut response = vec![FunctionCode::WriteSingleCoil as u8];
298                response.extend_from_slice(&address.to_be_bytes());
299                response
300                    .extend_from_slice(&(if value { 0xFF00u16 } else { 0x0000u16 }).to_be_bytes());
301                response
302            }
303            Self::WriteSingleRegisterAck { address, value } => {
304                let mut response = vec![FunctionCode::WriteSingleRegister as u8];
305                response.extend_from_slice(&address.to_be_bytes());
306                response.extend_from_slice(&value.to_be_bytes());
307                response
308            }
309            Self::WriteMultipleAck {
310                function,
311                address,
312                quantity,
313            } => {
314                let mut response = vec![function as u8];
315                response.extend_from_slice(&address.to_be_bytes());
316                response.extend_from_slice(&quantity.to_be_bytes());
317                response
318            }
319            Self::MaskWriteAck {
320                address,
321                and_mask,
322                or_mask,
323            } => {
324                let mut response = vec![FunctionCode::MaskWriteRegister as u8];
325                response.extend_from_slice(&address.to_be_bytes());
326                response.extend_from_slice(&and_mask.to_be_bytes());
327                response.extend_from_slice(&or_mask.to_be_bytes());
328                response
329            }
330            Self::Custom(response) => return Ok(response),
331        };
332
333        ResponsePdu::new(bytes)
334    }
335}
336
337fn parse_read_bits(function: FunctionCode, pdu: &[u8]) -> Result<SemanticRequest, ExceptionCode> {
338    ensure_len(pdu, 5)?;
339    let address = u16::from_be_bytes([pdu[1], pdu[2]]);
340    let quantity = u16::from_be_bytes([pdu[3], pdu[4]]);
341    ensure_quantity(quantity, 2000)?;
342    ensure_span(address, quantity)?;
343
344    Ok(match function {
345        FunctionCode::ReadCoils => SemanticRequest::ReadCoils { address, quantity },
346        FunctionCode::ReadDiscreteInputs => {
347            SemanticRequest::ReadDiscreteInputs { address, quantity }
348        }
349        _ => unreachable!("read bit parser only supports FC01/FC02"),
350    })
351}
352
353fn parse_read_registers(
354    function: FunctionCode,
355    pdu: &[u8],
356) -> Result<SemanticRequest, ExceptionCode> {
357    ensure_len(pdu, 5)?;
358    let address = u16::from_be_bytes([pdu[1], pdu[2]]);
359    let quantity = u16::from_be_bytes([pdu[3], pdu[4]]);
360    ensure_quantity(quantity, 125)?;
361    ensure_span(address, quantity)?;
362
363    Ok(match function {
364        FunctionCode::ReadHoldingRegisters => {
365            SemanticRequest::ReadHoldingRegisters { address, quantity }
366        }
367        FunctionCode::ReadInputRegisters => {
368            SemanticRequest::ReadInputRegisters { address, quantity }
369        }
370        _ => unreachable!("read register parser only supports FC03/FC04"),
371    })
372}
373
374fn parse_write_single_coil(pdu: &[u8]) -> Result<SemanticRequest, ExceptionCode> {
375    ensure_len(pdu, 5)?;
376    let address = u16::from_be_bytes([pdu[1], pdu[2]]);
377    let raw_value = u16::from_be_bytes([pdu[3], pdu[4]]);
378    let value = match raw_value {
379        0x0000 => false,
380        0xFF00 => true,
381        _ => return Err(ExceptionCode::IllegalDataValue),
382    };
383
384    Ok(SemanticRequest::WriteSingleCoil { address, value })
385}
386
387fn parse_write_single_register(pdu: &[u8]) -> Result<SemanticRequest, ExceptionCode> {
388    ensure_len(pdu, 5)?;
389    let address = u16::from_be_bytes([pdu[1], pdu[2]]);
390    let value = u16::from_be_bytes([pdu[3], pdu[4]]);
391    Ok(SemanticRequest::WriteSingleRegister { address, value })
392}
393
394fn parse_write_multiple_coils(pdu: &[u8]) -> Result<SemanticRequest, ExceptionCode> {
395    ensure_len(pdu, 6)?;
396    let address = u16::from_be_bytes([pdu[1], pdu[2]]);
397    let quantity = u16::from_be_bytes([pdu[3], pdu[4]]);
398    ensure_quantity(quantity, 1968)?;
399    ensure_span(address, quantity)?;
400
401    let byte_count = pdu[5] as usize;
402    let expected = quantity.div_ceil(8) as usize;
403    if byte_count != expected || pdu.len() < 6 + byte_count {
404        return Err(ExceptionCode::IllegalDataValue);
405    }
406
407    let values = unpack_bits(&pdu[6..6 + byte_count], quantity as usize);
408    Ok(SemanticRequest::WriteMultipleCoils { address, values })
409}
410
411fn parse_write_multiple_registers(pdu: &[u8]) -> Result<SemanticRequest, ExceptionCode> {
412    ensure_len(pdu, 6)?;
413    let address = u16::from_be_bytes([pdu[1], pdu[2]]);
414    let quantity = u16::from_be_bytes([pdu[3], pdu[4]]);
415    ensure_quantity(quantity, 123)?;
416    ensure_span(address, quantity)?;
417
418    let byte_count = pdu[5] as usize;
419    let expected = quantity as usize * 2;
420    if byte_count != expected || pdu.len() < 6 + byte_count {
421        return Err(ExceptionCode::IllegalDataValue);
422    }
423
424    let mut values = Vec::with_capacity(quantity as usize);
425    for chunk in pdu[6..6 + byte_count].chunks_exact(2) {
426        values.push(u16::from_be_bytes([chunk[0], chunk[1]]));
427    }
428
429    Ok(SemanticRequest::WriteMultipleRegisters { address, values })
430}
431
432fn parse_mask_write_register(pdu: &[u8]) -> Result<SemanticRequest, ExceptionCode> {
433    ensure_len(pdu, 7)?;
434    let address = u16::from_be_bytes([pdu[1], pdu[2]]);
435    let and_mask = u16::from_be_bytes([pdu[3], pdu[4]]);
436    let or_mask = u16::from_be_bytes([pdu[5], pdu[6]]);
437    Ok(SemanticRequest::MaskWriteRegister {
438        address,
439        and_mask,
440        or_mask,
441    })
442}
443
444fn parse_read_write_multiple_registers(pdu: &[u8]) -> Result<SemanticRequest, ExceptionCode> {
445    ensure_len(pdu, 10)?;
446    let read_address = u16::from_be_bytes([pdu[1], pdu[2]]);
447    let read_quantity = u16::from_be_bytes([pdu[3], pdu[4]]);
448    ensure_quantity(read_quantity, 125)?;
449    ensure_span(read_address, read_quantity)?;
450
451    let write_address = u16::from_be_bytes([pdu[5], pdu[6]]);
452    let write_quantity = u16::from_be_bytes([pdu[7], pdu[8]]);
453    ensure_quantity(write_quantity, 121)?;
454    ensure_span(write_address, write_quantity)?;
455
456    let byte_count = pdu[9] as usize;
457    let expected = write_quantity as usize * 2;
458    if byte_count != expected || pdu.len() < 10 + byte_count {
459        return Err(ExceptionCode::IllegalDataValue);
460    }
461
462    let mut values = Vec::with_capacity(write_quantity as usize);
463    for chunk in pdu[10..10 + byte_count].chunks_exact(2) {
464        values.push(u16::from_be_bytes([chunk[0], chunk[1]]));
465    }
466
467    Ok(SemanticRequest::ReadWriteMultipleRegisters {
468        read_address,
469        read_quantity,
470        write_address,
471        values,
472    })
473}
474
475fn ensure_len(pdu: &[u8], minimum: usize) -> Result<(), ExceptionCode> {
476    if pdu.len() < minimum {
477        Err(ExceptionCode::IllegalDataValue)
478    } else {
479        Ok(())
480    }
481}
482
483fn ensure_quantity(quantity: u16, maximum: u16) -> Result<(), ExceptionCode> {
484    if quantity == 0 || quantity > maximum {
485        Err(ExceptionCode::IllegalDataValue)
486    } else {
487        Ok(())
488    }
489}
490
491fn ensure_span(address: u16, quantity: u16) -> Result<(), ExceptionCode> {
492    if quantity == 0 {
493        return Err(ExceptionCode::IllegalDataValue);
494    }
495
496    let end = address as u32 + quantity as u32 - 1;
497    if end > u16::MAX as u32 {
498        Err(ExceptionCode::IllegalDataAddress)
499    } else {
500        Ok(())
501    }
502}
503
504fn unpack_bits(data: &[u8], quantity: usize) -> Vec<bool> {
505    let mut values = Vec::with_capacity(quantity);
506    for index in 0..quantity {
507        values.push((data[index / 8] & (1 << (index % 8))) != 0);
508    }
509    values
510}
511
512fn pack_bits(values: &[bool]) -> Vec<u8> {
513    let mut packed = vec![0u8; values.len().div_ceil(8)];
514    for (index, value) in values.iter().enumerate() {
515        if *value {
516            packed[index / 8] |= 1 << (index % 8);
517        }
518    }
519    packed
520}
521
522#[cfg(test)]
523mod tests {
524    use super::{ExceptionCode, FunctionCode, RequestPdu, SemanticRequest, SemanticResponse};
525
526    #[test]
527    fn semantic_parser_enforces_broadcast_legality() {
528        let request =
529            RequestPdu::new([FunctionCode::ReadHoldingRegisters as u8, 0, 0, 0, 1]).unwrap();
530        let error = request.semantic_request(true).unwrap_err();
531        assert_eq!(error, ExceptionCode::IllegalFunction);
532    }
533
534    #[test]
535    fn semantic_parser_accepts_custom_functions() {
536        let request = RequestPdu::new([0x41, 0xAA, 0xBB]).unwrap();
537        let semantic = request.semantic_request(false).unwrap();
538        assert_eq!(
539            semantic,
540            SemanticRequest::Custom {
541                function_code: 0x41,
542                payload: vec![0xAA, 0xBB],
543            }
544        );
545    }
546
547    #[test]
548    fn semantic_response_packs_bits() {
549        let response = SemanticResponse::Bits {
550            function: FunctionCode::ReadCoils,
551            values: vec![true, false, true, true, false, false, false, false, true],
552        }
553        .encode()
554        .unwrap();
555
556        assert_eq!(response.as_bytes(), &[0x01, 0x02, 0x0D, 0x01]);
557    }
558
559    #[test]
560    fn parser_rejects_invalid_register_write_lengths() {
561        let request = RequestPdu::new([0x10, 0x00, 0x02, 0x00, 0x02, 0x03, 0xAA, 0xBB]).unwrap();
562        let error = request.semantic_request(false).unwrap_err();
563        assert_eq!(error, ExceptionCode::IllegalDataValue);
564    }
565}