Skip to main content

stackforge_core/layer/modbus/
builder.rs

1//! Modbus packet builder.
2//!
3//! Provides a fluent API for constructing Modbus/TCP, RTU, and ASCII frames.
4//!
5//! # Examples
6//!
7//! ```rust
8//! use stackforge_core::layer::modbus::builder::ModbusBuilder;
9//!
10//! // Read Coils Request (Modbus/TCP)
11//! let pkt = ModbusBuilder::new()
12//!     .trans_id(1)
13//!     .unit_id(1)
14//!     .func_code(0x01)
15//!     .start_addr(0x0000)
16//!     .quantity(10)
17//!     .build();
18//! assert_eq!(pkt.len(), 12);
19//! ```
20
21use super::ModbusFrameType;
22use super::crc::{modbus_crc16, modbus_lrc};
23
24/// Builder for Modbus packets.
25///
26/// Supports three frame types:
27/// - **TCP (MBAP)**: Default. Adds a 7-byte MBAP header.
28/// - **RTU**: Binary serial framing with CRC-16 appended.
29/// - **ASCII**: Hex-encoded with ':' prefix, LRC, and CRLF suffix.
30#[derive(Debug, Clone)]
31pub struct ModbusBuilder {
32    frame_type: ModbusFrameType,
33    trans_id: u16,
34    proto_id: u16,
35    unit_id: u8,
36    func_code: u8,
37    /// Raw PDU data bytes (after function code).
38    pdu_data: Vec<u8>,
39    // Convenience fields for common request types
40    start_addr: Option<u16>,
41    quantity: Option<u16>,
42    output_value: Option<u16>,
43    values: Vec<u16>,
44    coil_values: Vec<bool>,
45    sub_func: Option<u16>,
46    and_mask: Option<u16>,
47    or_mask: Option<u16>,
48    /// Extra data bytes for diagnostics, file records, etc.
49    extra_data: Vec<u8>,
50}
51
52impl Default for ModbusBuilder {
53    fn default() -> Self {
54        Self {
55            frame_type: ModbusFrameType::Tcp,
56            trans_id: 0,
57            proto_id: 0,
58            unit_id: 0,
59            func_code: 0,
60            pdu_data: Vec::new(),
61            start_addr: None,
62            quantity: None,
63            output_value: None,
64            values: Vec::new(),
65            coil_values: Vec::new(),
66            sub_func: None,
67            and_mask: None,
68            or_mask: None,
69            extra_data: Vec::new(),
70        }
71    }
72}
73
74impl ModbusBuilder {
75    /// Create a new Modbus builder with TCP (MBAP) framing.
76    #[must_use]
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    // ========================================================================
82    // Frame type setters
83    // ========================================================================
84
85    /// Use Modbus/TCP (MBAP) framing (default).
86    #[must_use]
87    pub fn tcp(mut self) -> Self {
88        self.frame_type = ModbusFrameType::Tcp;
89        self
90    }
91
92    /// Use Modbus RTU (serial binary) framing.
93    #[must_use]
94    pub fn rtu(mut self) -> Self {
95        self.frame_type = ModbusFrameType::Rtu;
96        self
97    }
98
99    /// Use Modbus ASCII (serial hex-encoded) framing.
100    #[must_use]
101    pub fn ascii(mut self) -> Self {
102        self.frame_type = ModbusFrameType::Ascii;
103        self
104    }
105
106    // ========================================================================
107    // Field setters
108    // ========================================================================
109
110    /// Set the Transaction ID (MBAP only).
111    #[must_use]
112    pub fn trans_id(mut self, id: u16) -> Self {
113        self.trans_id = id;
114        self
115    }
116
117    /// Set the Protocol ID (MBAP only; should be 0x0000).
118    #[must_use]
119    pub fn proto_id(mut self, id: u16) -> Self {
120        self.proto_id = id;
121        self
122    }
123
124    /// Set the Unit ID / Slave Address.
125    #[must_use]
126    pub fn unit_id(mut self, id: u8) -> Self {
127        self.unit_id = id;
128        self
129    }
130
131    /// Set the Function Code.
132    #[must_use]
133    pub fn func_code(mut self, fc: u8) -> Self {
134        self.func_code = fc;
135        self
136    }
137
138    /// Set the Start Address (for Read/Write requests).
139    #[must_use]
140    pub fn start_addr(mut self, addr: u16) -> Self {
141        self.start_addr = Some(addr);
142        self
143    }
144
145    /// Set the Quantity (for Read requests).
146    #[must_use]
147    pub fn quantity(mut self, qty: u16) -> Self {
148        self.quantity = Some(qty);
149        self
150    }
151
152    /// Set the Output Value (for Write Single Coil/Register).
153    #[must_use]
154    pub fn output_value(mut self, val: u16) -> Self {
155        self.output_value = Some(val);
156        self
157    }
158
159    /// Set register values (for Write Multiple Registers).
160    #[must_use]
161    pub fn values(mut self, vals: Vec<u16>) -> Self {
162        self.values = vals;
163        self
164    }
165
166    /// Set coil values (for Write Multiple Coils).
167    #[must_use]
168    pub fn coil_values(mut self, vals: Vec<bool>) -> Self {
169        self.coil_values = vals;
170        self
171    }
172
173    /// Set the Sub-function code (for Diagnostics 0x08).
174    #[must_use]
175    pub fn sub_func(mut self, sf: u16) -> Self {
176        self.sub_func = Some(sf);
177        self
178    }
179
180    /// Set the AND mask (for Mask Write Register 0x16).
181    #[must_use]
182    pub fn and_mask(mut self, mask: u16) -> Self {
183        self.and_mask = Some(mask);
184        self
185    }
186
187    /// Set the OR mask (for Mask Write Register 0x16).
188    #[must_use]
189    pub fn or_mask(mut self, mask: u16) -> Self {
190        self.or_mask = Some(mask);
191        self
192    }
193
194    /// Set raw PDU data (after the function code).
195    /// This overrides the automatic PDU building.
196    #[must_use]
197    pub fn pdu_data(mut self, data: Vec<u8>) -> Self {
198        self.pdu_data = data;
199        self
200    }
201
202    /// Set extra data bytes (for diagnostics data field, etc.).
203    #[must_use]
204    pub fn extra_data(mut self, data: Vec<u8>) -> Self {
205        self.extra_data = data;
206        self
207    }
208
209    // ========================================================================
210    // Build
211    // ========================================================================
212
213    /// Build the PDU data bytes (everything after the function code).
214    fn build_pdu(&self) -> Vec<u8> {
215        // If raw PDU data is set, use it directly
216        if !self.pdu_data.is_empty() {
217            return self.pdu_data.clone();
218        }
219
220        let mut pdu = Vec::new();
221
222        match self.func_code {
223            // Read Coils, Read Discrete Inputs, Read Holding Registers, Read Input Registers
224            0x01..=0x04 => {
225                let addr = self.start_addr.unwrap_or(0);
226                let qty = self.quantity.unwrap_or(1);
227                pdu.extend_from_slice(&addr.to_be_bytes());
228                pdu.extend_from_slice(&qty.to_be_bytes());
229            },
230            // Write Single Coil
231            0x05 => {
232                let addr = self.start_addr.unwrap_or(0);
233                let val = self.output_value.unwrap_or(0xFF00);
234                pdu.extend_from_slice(&addr.to_be_bytes());
235                pdu.extend_from_slice(&val.to_be_bytes());
236            },
237            // Write Single Register
238            0x06 => {
239                let addr = self.start_addr.unwrap_or(0);
240                let val = self.output_value.unwrap_or(0);
241                pdu.extend_from_slice(&addr.to_be_bytes());
242                pdu.extend_from_slice(&val.to_be_bytes());
243            },
244            // Read Exception Status, Report Slave ID
245            0x07 | 0x11 => {
246                // No data bytes in request
247            },
248            // Diagnostics
249            0x08 => {
250                let sf = self.sub_func.unwrap_or(0);
251                pdu.extend_from_slice(&sf.to_be_bytes());
252                pdu.extend_from_slice(&self.extra_data);
253            },
254            // Get Comm Event Counter, Get Comm Event Log
255            0x0B | 0x0C => {
256                // No data bytes in request
257            },
258            // Write Multiple Coils
259            0x0F => {
260                let addr = self.start_addr.unwrap_or(0);
261                let qty = if self.coil_values.is_empty() {
262                    self.quantity.unwrap_or(0)
263                } else {
264                    self.coil_values.len() as u16
265                };
266                pdu.extend_from_slice(&addr.to_be_bytes());
267                pdu.extend_from_slice(&qty.to_be_bytes());
268
269                if self.coil_values.is_empty() {
270                    pdu.push(0); // byte count = 0
271                } else {
272                    // Pack booleans into bytes (LSB first)
273                    let byte_count = self.coil_values.len().div_ceil(8);
274                    pdu.push(byte_count as u8);
275                    let mut bytes = vec![0u8; byte_count];
276                    for (i, &coil) in self.coil_values.iter().enumerate() {
277                        if coil {
278                            bytes[i / 8] |= 1 << (i % 8);
279                        }
280                    }
281                    pdu.extend_from_slice(&bytes);
282                }
283            },
284            // Write Multiple Registers
285            0x10 => {
286                let addr = self.start_addr.unwrap_or(0);
287                let qty = if self.values.is_empty() {
288                    self.quantity.unwrap_or(0)
289                } else {
290                    self.values.len() as u16
291                };
292                pdu.extend_from_slice(&addr.to_be_bytes());
293                pdu.extend_from_slice(&qty.to_be_bytes());
294
295                let byte_count = (self.values.len() * 2) as u8;
296                pdu.push(byte_count);
297                for &val in &self.values {
298                    pdu.extend_from_slice(&val.to_be_bytes());
299                }
300            },
301            // Mask Write Register
302            0x16 => {
303                let addr = self.start_addr.unwrap_or(0);
304                let and = self.and_mask.unwrap_or(0xFFFF);
305                let or = self.or_mask.unwrap_or(0x0000);
306                pdu.extend_from_slice(&addr.to_be_bytes());
307                pdu.extend_from_slice(&and.to_be_bytes());
308                pdu.extend_from_slice(&or.to_be_bytes());
309            },
310            // Read/Write Multiple Registers
311            0x17 => {
312                // Read part
313                let read_addr = self.start_addr.unwrap_or(0);
314                let read_qty = self.quantity.unwrap_or(0);
315                pdu.extend_from_slice(&read_addr.to_be_bytes());
316                pdu.extend_from_slice(&read_qty.to_be_bytes());
317                // Write part: use extra_data for write address + quantity + byte_count + values
318                pdu.extend_from_slice(&self.extra_data);
319            },
320            // Read FIFO Queue
321            0x18 => {
322                let addr = self.start_addr.unwrap_or(0);
323                pdu.extend_from_slice(&addr.to_be_bytes());
324            },
325            // Encapsulated Interface Transport (MEI)
326            0x2B => {
327                pdu.extend_from_slice(&self.extra_data);
328            },
329            // Default: no automatic PDU data
330            _ => {
331                pdu.extend_from_slice(&self.extra_data);
332            },
333        }
334
335        pdu
336    }
337
338    /// Compute the header size for this builder.
339    #[must_use]
340    pub fn header_size(&self) -> usize {
341        match self.frame_type {
342            ModbusFrameType::Tcp => {
343                // MBAP (7) + func_code (1) + PDU data
344                7 + 1 + self.build_pdu().len()
345            },
346            ModbusFrameType::Rtu => {
347                // slave (1) + func_code (1) + PDU data + CRC (2)
348                1 + 1 + self.build_pdu().len() + 2
349            },
350            ModbusFrameType::Ascii => {
351                // ':' + hex(slave + fc + pdu + lrc) + CR + LF
352                let inner_len = 1 + 1 + self.build_pdu().len() + 1; // slave + fc + pdu + lrc
353                1 + inner_len * 2 + 2
354            },
355        }
356    }
357
358    /// Build the Modbus frame into bytes.
359    #[must_use]
360    pub fn build(&self) -> Vec<u8> {
361        let pdu = self.build_pdu();
362
363        match self.frame_type {
364            ModbusFrameType::Tcp => {
365                let mut buf = Vec::with_capacity(7 + 1 + pdu.len());
366                // MBAP header
367                buf.extend_from_slice(&self.trans_id.to_be_bytes()); // Transaction ID
368                buf.extend_from_slice(&self.proto_id.to_be_bytes()); // Protocol ID
369                let length = (1 + 1 + pdu.len()) as u16; // unit_id + func_code + pdu_data
370                buf.extend_from_slice(&length.to_be_bytes()); // Length
371                buf.push(self.unit_id); // Unit ID
372                buf.push(self.func_code); // Function Code
373                buf.extend_from_slice(&pdu); // PDU data
374                buf
375            },
376            ModbusFrameType::Rtu => {
377                let mut frame = Vec::with_capacity(1 + 1 + pdu.len() + 2);
378                frame.push(self.unit_id); // Slave Address
379                frame.push(self.func_code); // Function Code
380                frame.extend_from_slice(&pdu); // Data
381                let crc = modbus_crc16(&frame);
382                frame.push((crc & 0xFF) as u8); // CRC low byte
383                frame.push((crc >> 8) as u8); // CRC high byte
384                frame
385            },
386            ModbusFrameType::Ascii => {
387                let mut inner = Vec::new();
388                inner.push(self.unit_id);
389                inner.push(self.func_code);
390                inner.extend_from_slice(&pdu);
391                let lrc = modbus_lrc(&inner);
392                inner.push(lrc);
393
394                // Encode as ASCII hex
395                let mut buf = Vec::with_capacity(1 + inner.len() * 2 + 2);
396                buf.push(b':');
397                for &byte in &inner {
398                    buf.push(hex_char(byte >> 4));
399                    buf.push(hex_char(byte & 0x0F));
400                }
401                buf.push(b'\r');
402                buf.push(b'\n');
403                buf
404            },
405        }
406    }
407}
408
409/// Convert a nibble (0-15) to its ASCII hex character (uppercase).
410fn hex_char(nibble: u8) -> u8 {
411    match nibble {
412        0..=9 => b'0' + nibble,
413        10..=15 => b'A' + (nibble - 10),
414        _ => b'?',
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use crate::layer::modbus::crc::{verify_crc16, verify_lrc};
422    use crate::layer::modbus::{ModbusLayer, is_modbus_tcp_payload};
423    use crate::layer::{LayerIndex, LayerKind};
424
425    #[test]
426    fn test_read_coils_tcp() {
427        let pkt = ModbusBuilder::new()
428            .trans_id(1)
429            .unit_id(1)
430            .func_code(0x01)
431            .start_addr(0x0000)
432            .quantity(10)
433            .build();
434
435        assert_eq!(pkt.len(), 12);
436        assert!(is_modbus_tcp_payload(&pkt));
437
438        let idx = LayerIndex::new(LayerKind::Modbus, 0, pkt.len());
439        let layer = ModbusLayer::new(idx);
440        assert_eq!(layer.trans_id(&pkt).unwrap(), 1);
441        assert_eq!(layer.unit_id(&pkt).unwrap(), 1);
442        assert_eq!(layer.func_code(&pkt).unwrap(), 0x01);
443        assert_eq!(layer.start_addr(&pkt).unwrap(), 0);
444        assert_eq!(layer.quantity(&pkt).unwrap(), 10);
445    }
446
447    #[test]
448    fn test_write_single_coil_tcp() {
449        let pkt = ModbusBuilder::new()
450            .trans_id(2)
451            .unit_id(1)
452            .func_code(0x05)
453            .start_addr(0x0013)
454            .output_value(0xFF00)
455            .build();
456
457        assert_eq!(pkt.len(), 12);
458
459        let idx = LayerIndex::new(LayerKind::Modbus, 0, pkt.len());
460        let layer = ModbusLayer::new(idx);
461        assert_eq!(layer.func_code(&pkt).unwrap(), 0x05);
462        assert_eq!(layer.start_addr(&pkt).unwrap(), 0x0013);
463        assert_eq!(layer.output_value(&pkt).unwrap(), 0xFF00);
464    }
465
466    #[test]
467    fn test_write_multiple_registers_tcp() {
468        let pkt = ModbusBuilder::new()
469            .trans_id(3)
470            .unit_id(1)
471            .func_code(0x10)
472            .start_addr(0x0001)
473            .values(vec![0x000A, 0x0102])
474            .build();
475
476        let idx = LayerIndex::new(LayerKind::Modbus, 0, pkt.len());
477        let layer = ModbusLayer::new(idx);
478        assert_eq!(layer.func_code(&pkt).unwrap(), 0x10);
479        assert_eq!(layer.start_addr(&pkt).unwrap(), 0x0001);
480        // quantity (from values.len())
481        assert_eq!(layer.quantity(&pkt).unwrap(), 2);
482    }
483
484    #[test]
485    fn test_mask_write_register_tcp() {
486        let pkt = ModbusBuilder::new()
487            .trans_id(1)
488            .unit_id(1)
489            .func_code(0x16)
490            .start_addr(0x0004)
491            .and_mask(0x00F2)
492            .or_mask(0x0025)
493            .build();
494
495        let idx = LayerIndex::new(LayerKind::Modbus, 0, pkt.len());
496        let layer = ModbusLayer::new(idx);
497        assert_eq!(layer.func_code(&pkt).unwrap(), 0x16);
498        assert_eq!(layer.ref_addr(&pkt).unwrap(), 0x0004);
499        assert_eq!(layer.and_mask(&pkt).unwrap(), 0x00F2);
500        assert_eq!(layer.or_mask(&pkt).unwrap(), 0x0025);
501    }
502
503    #[test]
504    fn test_rtu_frame() {
505        let pkt = ModbusBuilder::new()
506            .rtu()
507            .unit_id(1)
508            .func_code(0x03)
509            .start_addr(0x0000)
510            .quantity(10)
511            .build();
512
513        // slave(1) + fc(1) + addr(2) + qty(2) + crc(2) = 8
514        assert_eq!(pkt.len(), 8);
515        assert_eq!(pkt[0], 1); // slave addr
516        assert_eq!(pkt[1], 0x03); // func code
517        assert!(verify_crc16(&pkt));
518    }
519
520    #[test]
521    fn test_ascii_frame() {
522        let pkt = ModbusBuilder::new()
523            .ascii()
524            .unit_id(1)
525            .func_code(0x03)
526            .start_addr(0x0000)
527            .quantity(10)
528            .build();
529
530        // Should start with ':' and end with CRLF
531        assert_eq!(pkt[0], b':');
532        assert_eq!(pkt[pkt.len() - 2], b'\r');
533        assert_eq!(pkt[pkt.len() - 1], b'\n');
534
535        // Decode the hex content and verify LRC
536        let hex_str = &pkt[1..pkt.len() - 2];
537        let mut decoded = Vec::new();
538        for chunk in hex_str.chunks(2) {
539            let high = from_hex_char(chunk[0]).unwrap();
540            let low = from_hex_char(chunk[1]).unwrap();
541            decoded.push((high << 4) | low);
542        }
543        assert!(verify_lrc(&decoded));
544    }
545
546    #[test]
547    fn test_default_builder() {
548        let pkt = ModbusBuilder::new().build();
549        // MBAP header (7) + func_code (1) = 8 bytes minimum
550        assert_eq!(pkt.len(), 8);
551        assert_eq!(pkt[7], 0); // func_code = 0
552    }
553
554    #[test]
555    fn test_raw_pdu_data() {
556        let pkt = ModbusBuilder::new()
557            .trans_id(1)
558            .unit_id(1)
559            .func_code(0x03)
560            .pdu_data(vec![0x00, 0x00, 0x00, 0x0A])
561            .build();
562
563        let idx = LayerIndex::new(LayerKind::Modbus, 0, pkt.len());
564        let layer = ModbusLayer::new(idx);
565        assert_eq!(layer.func_code(&pkt).unwrap(), 0x03);
566        assert_eq!(layer.start_addr(&pkt).unwrap(), 0x0000);
567        assert_eq!(layer.quantity(&pkt).unwrap(), 0x000A);
568    }
569
570    #[test]
571    fn test_write_multiple_coils_tcp() {
572        let pkt = ModbusBuilder::new()
573            .trans_id(1)
574            .unit_id(1)
575            .func_code(0x0F)
576            .start_addr(0x0013)
577            .coil_values(vec![
578                true, false, true, true, false, false, true, true, true, false,
579            ])
580            .build();
581
582        let idx = LayerIndex::new(LayerKind::Modbus, 0, pkt.len());
583        let layer = ModbusLayer::new(idx);
584        assert_eq!(layer.func_code(&pkt).unwrap(), 0x0F);
585        assert_eq!(layer.start_addr(&pkt).unwrap(), 0x0013);
586        // Quantity = 10 coils
587        assert_eq!(layer.quantity(&pkt).unwrap(), 10);
588    }
589
590    #[test]
591    fn test_round_trip_read_holding_registers() {
592        let original = ModbusBuilder::new()
593            .trans_id(42)
594            .unit_id(0x11)
595            .func_code(0x03)
596            .start_addr(0x006B)
597            .quantity(3)
598            .build();
599
600        assert!(is_modbus_tcp_payload(&original));
601
602        let idx = LayerIndex::new(LayerKind::Modbus, 0, original.len());
603        let layer = ModbusLayer::new(idx);
604        assert_eq!(layer.trans_id(&original).unwrap(), 42);
605        assert_eq!(layer.unit_id(&original).unwrap(), 0x11);
606        assert_eq!(layer.func_code(&original).unwrap(), 0x03);
607        assert_eq!(layer.start_addr(&original).unwrap(), 0x006B);
608        assert_eq!(layer.quantity(&original).unwrap(), 3);
609    }
610
611    /// Helper to decode a hex character.
612    fn from_hex_char(c: u8) -> Option<u8> {
613        match c {
614            b'0'..=b'9' => Some(c - b'0'),
615            b'A'..=b'F' => Some(c - b'A' + 10),
616            b'a'..=b'f' => Some(c - b'a' + 10),
617            _ => None,
618        }
619    }
620}