Skip to main content

oxirs_modbus/
function_code_handler.rs

1//! Modbus function code dispatch table.
2//!
3//! Provides a registry of handlers for each Modbus function code, dispatching
4//! incoming PDU requests to the correct handler and returning PDU responses.
5
6use std::collections::HashMap;
7
8// ---------------------------------------------------------------------------
9// Modbus Function Code enum
10// ---------------------------------------------------------------------------
11
12/// The nine standard Modbus function codes supported by this implementation.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(u8)]
15pub enum FunctionCode {
16    ReadCoils = 0x01,
17    ReadDiscreteInputs = 0x02,
18    ReadHoldingRegisters = 0x03,
19    ReadInputRegisters = 0x04,
20    WriteSingleCoil = 0x05,
21    WriteSingleRegister = 0x06,
22    WriteMultipleCoils = 0x0F,
23    WriteMultipleRegisters = 0x10,
24    ReadDeviceIdentification = 0x2B,
25}
26
27impl FunctionCode {
28    /// Return the raw byte value.
29    pub fn as_u8(self) -> u8 {
30        self as u8
31    }
32
33    /// Try to convert a raw byte to a `FunctionCode`.
34    pub fn from_u8(v: u8) -> Option<Self> {
35        match v {
36            0x01 => Some(Self::ReadCoils),
37            0x02 => Some(Self::ReadDiscreteInputs),
38            0x03 => Some(Self::ReadHoldingRegisters),
39            0x04 => Some(Self::ReadInputRegisters),
40            0x05 => Some(Self::WriteSingleCoil),
41            0x06 => Some(Self::WriteSingleRegister),
42            0x0F => Some(Self::WriteMultipleCoils),
43            0x10 => Some(Self::WriteMultipleRegisters),
44            0x2B => Some(Self::ReadDeviceIdentification),
45            _ => None,
46        }
47    }
48
49    /// Human-readable name for logging.
50    pub fn name(self) -> &'static str {
51        match self {
52            Self::ReadCoils => "ReadCoils",
53            Self::ReadDiscreteInputs => "ReadDiscreteInputs",
54            Self::ReadHoldingRegisters => "ReadHoldingRegisters",
55            Self::ReadInputRegisters => "ReadInputRegisters",
56            Self::WriteSingleCoil => "WriteSingleCoil",
57            Self::WriteSingleRegister => "WriteSingleRegister",
58            Self::WriteMultipleCoils => "WriteMultipleCoils",
59            Self::WriteMultipleRegisters => "WriteMultipleRegisters",
60            Self::ReadDeviceIdentification => "ReadDeviceIdentification",
61        }
62    }
63}
64
65// ---------------------------------------------------------------------------
66// Request / Response PDU types
67// ---------------------------------------------------------------------------
68
69/// A raw Modbus function code request PDU (function code byte + data bytes).
70#[derive(Debug, Clone)]
71pub struct FunctionCodeRequest {
72    /// Function code byte.
73    pub code: u8,
74    /// Request data bytes (excluding the function code byte).
75    pub data: Vec<u8>,
76}
77
78/// A raw Modbus function code response PDU.
79#[derive(Debug, Clone)]
80pub struct FunctionCodeResponse {
81    /// Function code byte (may have bit 7 set on error).
82    pub code: u8,
83    /// Response data bytes.
84    pub data: Vec<u8>,
85    /// `true` when the response carries an exception code (code has bit 7 set).
86    pub is_error: bool,
87}
88
89// ---------------------------------------------------------------------------
90// Handler trait
91// ---------------------------------------------------------------------------
92
93/// Trait for objects that can handle a specific Modbus function code.
94pub trait FunctionCodeHandler: Send + Sync {
95    /// Process the request and return a response.
96    fn handle(&self, req: &FunctionCodeRequest) -> FunctionCodeResponse;
97
98    /// The Modbus function code byte this handler is responsible for.
99    fn function_code(&self) -> u8;
100
101    /// Short textual description of what this handler does.
102    fn description(&self) -> &str;
103}
104
105// ---------------------------------------------------------------------------
106// Dispatch table
107// ---------------------------------------------------------------------------
108
109/// A mapping from function code bytes to boxed `FunctionCodeHandler`s.
110pub struct DispatchTable {
111    handlers: HashMap<u8, Box<dyn FunctionCodeHandler>>,
112}
113
114impl Default for DispatchTable {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120impl DispatchTable {
121    /// Create an empty dispatch table.
122    pub fn new() -> Self {
123        DispatchTable {
124            handlers: HashMap::new(),
125        }
126    }
127
128    /// Create a dispatch table pre-loaded with echo handlers for all nine
129    /// standard function codes.
130    pub fn with_defaults() -> Self {
131        let mut table = Self::new();
132        let codes = [
133            FunctionCode::ReadCoils,
134            FunctionCode::ReadDiscreteInputs,
135            FunctionCode::ReadHoldingRegisters,
136            FunctionCode::ReadInputRegisters,
137            FunctionCode::WriteSingleCoil,
138            FunctionCode::WriteSingleRegister,
139            FunctionCode::WriteMultipleCoils,
140            FunctionCode::WriteMultipleRegisters,
141            FunctionCode::ReadDeviceIdentification,
142        ];
143        for fc in codes {
144            table.register(Box::new(EchoHandler { fc: fc.as_u8() }));
145        }
146        table
147    }
148
149    /// Register (or replace) a handler.
150    pub fn register(&mut self, handler: Box<dyn FunctionCodeHandler>) {
151        self.handlers.insert(handler.function_code(), handler);
152    }
153
154    /// Dispatch a request to its registered handler.
155    ///
156    /// If no handler is registered for `req.code`, an error response with
157    /// Modbus exception code `0x01` (Illegal Function) is returned.
158    pub fn dispatch(&self, req: &FunctionCodeRequest) -> FunctionCodeResponse {
159        if let Some(handler) = self.handlers.get(&req.code) {
160            handler.handle(req)
161        } else {
162            Self::error_response(req, 0x01) // Illegal Function
163        }
164    }
165
166    /// Return a sorted list of all supported function code bytes.
167    pub fn supported_codes(&self) -> Vec<u8> {
168        let mut codes: Vec<u8> = self.handlers.keys().copied().collect();
169        codes.sort_unstable();
170        codes
171    }
172
173    /// `true` if a handler is registered for `code`.
174    pub fn is_supported(&self, code: u8) -> bool {
175        self.handlers.contains_key(&code)
176    }
177
178    /// Number of registered handlers.
179    pub fn handler_count(&self) -> usize {
180        self.handlers.len()
181    }
182
183    /// Build an error response: function code byte has bit 7 set, and the
184    /// data contains the single `error_code` byte.
185    pub fn error_response(req: &FunctionCodeRequest, error_code: u8) -> FunctionCodeResponse {
186        FunctionCodeResponse {
187            code: req.code | 0x80,
188            data: vec![error_code],
189            is_error: true,
190        }
191    }
192}
193
194// ---------------------------------------------------------------------------
195// EchoHandler: returns request data unchanged
196// ---------------------------------------------------------------------------
197
198/// A simple handler that echoes the request data back as the response data.
199/// Useful for testing and as a default placeholder.
200pub struct EchoHandler {
201    /// The function code this handler responds to.
202    pub fc: u8,
203}
204
205impl FunctionCodeHandler for EchoHandler {
206    fn handle(&self, req: &FunctionCodeRequest) -> FunctionCodeResponse {
207        FunctionCodeResponse {
208            code: req.code,
209            data: req.data.clone(),
210            is_error: false,
211        }
212    }
213
214    fn function_code(&self) -> u8 {
215        self.fc
216    }
217
218    fn description(&self) -> &str {
219        "Echo handler (returns request data)"
220    }
221}
222
223// ---------------------------------------------------------------------------
224// Tests
225// ---------------------------------------------------------------------------
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    fn req(code: u8, data: &[u8]) -> FunctionCodeRequest {
232        FunctionCodeRequest {
233            code,
234            data: data.to_vec(),
235        }
236    }
237
238    // --- FunctionCode enum ---
239
240    #[test]
241    fn test_fc_read_coils_value() {
242        assert_eq!(FunctionCode::ReadCoils.as_u8(), 0x01);
243    }
244
245    #[test]
246    fn test_fc_read_discrete_inputs_value() {
247        assert_eq!(FunctionCode::ReadDiscreteInputs.as_u8(), 0x02);
248    }
249
250    #[test]
251    fn test_fc_read_holding_registers_value() {
252        assert_eq!(FunctionCode::ReadHoldingRegisters.as_u8(), 0x03);
253    }
254
255    #[test]
256    fn test_fc_read_input_registers_value() {
257        assert_eq!(FunctionCode::ReadInputRegisters.as_u8(), 0x04);
258    }
259
260    #[test]
261    fn test_fc_write_single_coil_value() {
262        assert_eq!(FunctionCode::WriteSingleCoil.as_u8(), 0x05);
263    }
264
265    #[test]
266    fn test_fc_write_single_register_value() {
267        assert_eq!(FunctionCode::WriteSingleRegister.as_u8(), 0x06);
268    }
269
270    #[test]
271    fn test_fc_write_multiple_coils_value() {
272        assert_eq!(FunctionCode::WriteMultipleCoils.as_u8(), 0x0F);
273    }
274
275    #[test]
276    fn test_fc_write_multiple_registers_value() {
277        assert_eq!(FunctionCode::WriteMultipleRegisters.as_u8(), 0x10);
278    }
279
280    #[test]
281    fn test_fc_read_device_identification_value() {
282        assert_eq!(FunctionCode::ReadDeviceIdentification.as_u8(), 0x2B);
283    }
284
285    #[test]
286    fn test_fc_from_u8_valid() {
287        assert_eq!(FunctionCode::from_u8(0x01), Some(FunctionCode::ReadCoils));
288        assert_eq!(
289            FunctionCode::from_u8(0x10),
290            Some(FunctionCode::WriteMultipleRegisters)
291        );
292    }
293
294    #[test]
295    fn test_fc_from_u8_invalid() {
296        assert_eq!(FunctionCode::from_u8(0x00), None);
297        assert_eq!(FunctionCode::from_u8(0xFF), None);
298    }
299
300    // --- DispatchTable ---
301
302    #[test]
303    fn test_dispatch_table_new_empty() {
304        let table = DispatchTable::new();
305        assert_eq!(table.handler_count(), 0);
306    }
307
308    #[test]
309    fn test_with_defaults_has_nine_handlers() {
310        let table = DispatchTable::with_defaults();
311        assert_eq!(table.handler_count(), 9);
312    }
313
314    #[test]
315    fn test_is_supported_true_for_defaults() {
316        let table = DispatchTable::with_defaults();
317        assert!(table.is_supported(0x01));
318        assert!(table.is_supported(0x2B));
319    }
320
321    #[test]
322    fn test_is_supported_false_for_unknown() {
323        let table = DispatchTable::with_defaults();
324        assert!(!table.is_supported(0x99));
325    }
326
327    #[test]
328    fn test_supported_codes_sorted() {
329        let table = DispatchTable::with_defaults();
330        let codes = table.supported_codes();
331        let mut sorted = codes.clone();
332        sorted.sort_unstable();
333        assert_eq!(codes, sorted);
334    }
335
336    #[test]
337    fn test_dispatch_known_fc_returns_non_error() {
338        let table = DispatchTable::with_defaults();
339        let r = req(0x01, &[0x00, 0x00, 0x00, 0x10]);
340        let resp = table.dispatch(&r);
341        assert!(!resp.is_error);
342        assert_eq!(resp.code, 0x01);
343    }
344
345    #[test]
346    fn test_dispatch_unknown_fc_returns_error() {
347        let table = DispatchTable::with_defaults();
348        let r = req(0x99, &[]);
349        let resp = table.dispatch(&r);
350        assert!(resp.is_error);
351        // Code should have bit 7 set: 0x99 | 0x80 = 0x99 (already set)
352        assert_eq!(resp.code, 0x99 | 0x80);
353    }
354
355    #[test]
356    fn test_error_response_sets_high_bit() {
357        let r = req(0x03, &[0x00, 0x10]);
358        let resp = DispatchTable::error_response(&r, 0x02);
359        assert_eq!(resp.code, 0x83); // 0x03 | 0x80
360        assert!(resp.is_error);
361        assert_eq!(resp.data, vec![0x02]);
362    }
363
364    #[test]
365    fn test_error_response_error_code_in_data() {
366        let r = req(0x01, &[]);
367        let resp = DispatchTable::error_response(&r, 0x03);
368        assert_eq!(resp.data, vec![0x03]);
369    }
370
371    #[test]
372    fn test_register_replaces_existing_handler() {
373        let mut table = DispatchTable::new();
374        table.register(Box::new(EchoHandler { fc: 0x01 }));
375        assert_eq!(table.handler_count(), 1);
376
377        // Register a different handler for the same fc
378        struct AlwaysErrorHandler;
379        impl FunctionCodeHandler for AlwaysErrorHandler {
380            fn handle(&self, req: &FunctionCodeRequest) -> FunctionCodeResponse {
381                DispatchTable::error_response(req, 0x04)
382            }
383            fn function_code(&self) -> u8 {
384                0x01
385            }
386            fn description(&self) -> &str {
387                "always error"
388            }
389        }
390        table.register(Box::new(AlwaysErrorHandler));
391        // Count should not increase
392        assert_eq!(table.handler_count(), 1);
393
394        let r = req(0x01, &[]);
395        let resp = table.dispatch(&r);
396        // The new handler produces an error
397        assert!(resp.is_error);
398    }
399
400    // --- EchoHandler ---
401
402    #[test]
403    fn test_echo_handler_echoes_data() {
404        let handler = EchoHandler { fc: 0x03 };
405        let r = req(0x03, &[0xAA, 0xBB, 0xCC]);
406        let resp = handler.handle(&r);
407        assert_eq!(resp.data, vec![0xAA, 0xBB, 0xCC]);
408        assert!(!resp.is_error);
409    }
410
411    #[test]
412    fn test_echo_handler_function_code() {
413        let handler = EchoHandler { fc: 0x06 };
414        assert_eq!(handler.function_code(), 0x06);
415    }
416
417    #[test]
418    fn test_echo_handler_description_non_empty() {
419        let handler = EchoHandler { fc: 0x01 };
420        assert!(!handler.description().is_empty());
421    }
422
423    #[test]
424    fn test_echo_handler_empty_data() {
425        let handler = EchoHandler { fc: 0x01 };
426        let r = req(0x01, &[]);
427        let resp = handler.handle(&r);
428        assert!(resp.data.is_empty());
429    }
430
431    #[test]
432    fn test_handler_count_after_multiple_registers() {
433        let mut table = DispatchTable::new();
434        table.register(Box::new(EchoHandler { fc: 0x01 }));
435        table.register(Box::new(EchoHandler { fc: 0x02 }));
436        table.register(Box::new(EchoHandler { fc: 0x03 }));
437        assert_eq!(table.handler_count(), 3);
438    }
439
440    #[test]
441    fn test_dispatch_fc0f_multiple_coils() {
442        let table = DispatchTable::with_defaults();
443        let r = req(0x0F, &[0x00, 0x10, 0x00, 0x03, 0x01, 0x05]);
444        let resp = table.dispatch(&r);
445        assert!(!resp.is_error);
446        assert_eq!(resp.code, 0x0F);
447    }
448
449    #[test]
450    fn test_dispatch_fc10_multiple_registers() {
451        let table = DispatchTable::with_defaults();
452        let r = req(
453            0x10,
454            &[0x00, 0x01, 0x00, 0x02, 0x04, 0x00, 0x0A, 0x01, 0x02],
455        );
456        let resp = table.dispatch(&r);
457        assert!(!resp.is_error);
458    }
459
460    #[test]
461    fn test_dispatch_fc2b_device_identification() {
462        let table = DispatchTable::with_defaults();
463        let r = req(0x2B, &[0x0E, 0x01, 0x00]);
464        let resp = table.dispatch(&r);
465        assert!(!resp.is_error);
466        assert_eq!(resp.code, 0x2B);
467    }
468
469    #[test]
470    fn test_default_dispatch_table() {
471        let table = DispatchTable::default();
472        assert_eq!(table.handler_count(), 0);
473    }
474
475    #[test]
476    fn test_fc_name() {
477        assert_eq!(FunctionCode::ReadCoils.name(), "ReadCoils");
478        assert_eq!(
479            FunctionCode::WriteMultipleRegisters.name(),
480            "WriteMultipleRegisters"
481        );
482    }
483
484    #[test]
485    fn test_supported_codes_contains_all_default_fcs() {
486        let table = DispatchTable::with_defaults();
487        let codes = table.supported_codes();
488        for fc in &[0x01u8, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0F, 0x10, 0x2B] {
489            assert!(codes.contains(fc), "Missing FC 0x{:02X}", fc);
490        }
491    }
492}