Skip to main content

rs_modbus/
types.rs

1use crate::error::ModbusError;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5
6#[derive(Debug, Clone)]
7pub struct ApplicationDataUnit {
8    pub transaction: Option<u16>,
9    pub unit: u8,
10    pub fc: u8,
11    pub data: Vec<u8>,
12}
13
14impl ApplicationDataUnit {
15    pub fn new(unit: u8, fc: u8, data: Vec<u8>) -> Self {
16        Self {
17            transaction: None,
18            unit,
19            fc,
20            data,
21        }
22    }
23
24    pub fn with_transaction(mut self, transaction: u16) -> Self {
25        self.transaction = Some(transaction);
26        self
27    }
28}
29
30#[derive(Debug, Clone)]
31pub struct FramedDataUnit {
32    pub adu: ApplicationDataUnit,
33    pub raw: Vec<u8>,
34}
35
36/// FC17 Server ID. Modbus V1.1b3 §6.17 leaves the Server ID length as
37/// device-specific (N bytes), so multi-byte IDs are supported via
38/// multi-byte. Mirrors njs-modbus `ServerId { serverId: number | number[] }`.
39#[derive(Debug, Clone)]
40pub struct ServerId {
41    /// Server ID bytes — typically 1 byte, but the spec allows N bytes.
42    pub server_id: Vec<u8>,
43    pub run_indicator_status: bool,
44    pub additional_data: Vec<u8>,
45}
46
47impl ServerId {
48    /// Convenience constructor for a single-byte Server ID.
49    pub fn single(id: u8, run_indicator: bool, additional_data: Vec<u8>) -> Self {
50        Self {
51            server_id: vec![id],
52            run_indicator_status: run_indicator,
53            additional_data,
54        }
55    }
56}
57
58#[derive(Debug, Clone)]
59pub struct DeviceIdentification {
60    pub read_device_id_code: u8,
61    pub conformity_level: u8,
62    pub more_follows: bool,
63    pub next_object_id: u8,
64    pub objects: Vec<DeviceObject>,
65}
66
67#[derive(Debug, Clone)]
68pub struct DeviceObject {
69    pub id: u8,
70    pub value: String,
71}
72
73#[derive(Debug, Clone, Default)]
74pub struct AddressRange {
75    pub discrete_inputs: Vec<(u16, u16)>,
76    pub coils: Vec<(u16, u16)>,
77    pub input_registers: Vec<(u16, u16)>,
78    pub holding_registers: Vec<(u16, u16)>,
79}
80
81/// Predictor result from a [`CustomFunctionCode`].
82///
83/// - `Length(n)` — total RTU frame length (PDU + CRC) is `n` bytes.
84/// - `NeedMore` — predictor can't decide yet; wait for more bytes.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum CustomFcPredict {
87    Length(usize),
88    NeedMore,
89}
90
91/// Async handler return for [`CustomFunctionCode::handle`].
92pub type CustomFcHandleResult =
93    Pin<Box<dyn Future<Output = Result<Vec<u8>, ModbusError>> + Send + 'static>>;
94
95/// Slave-side handler: receives PDU payload (bytes after `fc`, before CRC) and
96/// the unit ID being addressed; must return the PDU payload of the response.
97pub type CustomFcHandler = Arc<dyn Fn(Vec<u8>, u8) -> CustomFcHandleResult + Send + Sync + 'static>;
98
99/// Response from a Modbus master request, mirroring njs-modbus `ReturnValue<T>`.
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct MasterResponse<T> {
102    pub transaction: Option<u16>,
103    pub unit: u8,
104    pub fc: u8,
105    pub data: T,
106    pub raw: Vec<u8>,
107}
108
109impl<T: PartialEq> PartialEq<T> for MasterResponse<T> {
110    fn eq(&self, other: &T) -> bool {
111        self.data == *other
112    }
113}
114
115/// Defines a non-standard / user-defined Modbus function code. Mirrors
116/// njs-modbus `CustomFunctionCode`.
117///
118/// Registration paths:
119/// - [`crate::layers::application::RtuApplicationLayer::add_custom_function_code`] — framing only.
120/// - [`crate::slave::ModbusSlave::add_custom_function_code`] — framing + slave-side dispatch.
121/// - [`crate::master::ModbusMaster::add_custom_function_code`] + `send_custom_fc` — framing + request issuance.
122///
123/// The two `predict_*` callbacks declare how to derive the total RTU frame
124/// length (PDU + 2-byte CRC) from leading bytes; they are required so the
125/// framing FSM can advance without the deleted sliding-window CRC fallback.
126#[derive(Clone)]
127#[allow(clippy::type_complexity)]
128pub struct CustomFunctionCode {
129    /// Function code value (must fit in `u8`).
130    pub fc: u8,
131    /// Predict total RTU frame length for an incoming request (slave-side framing).
132    pub predict_request_length: Arc<dyn Fn(&[u8]) -> CustomFcPredict + Send + Sync + 'static>,
133    /// Predict total RTU frame length for an incoming response (master-side framing).
134    pub predict_response_length: Arc<dyn Fn(&[u8]) -> CustomFcPredict + Send + Sync + 'static>,
135    /// Slave-side handler. Returning `Err` is turned into a Modbus exception
136    /// response by the slave. If `handle` is `None`, the slave returns an
137    /// `ILLEGAL_FUNCTION` exception for this FC.
138    pub handle: Option<CustomFcHandler>,
139}
140
141impl std::fmt::Debug for CustomFunctionCode {
142    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
143        f.debug_struct("CustomFunctionCode")
144            .field("fc", &self.fc)
145            .field("handle", &self.handle.is_some())
146            .finish()
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_adu_new() {
156        let adu = ApplicationDataUnit::new(1, 0x03, vec![0x00, 0x00, 0x00, 0x0a]);
157        assert_eq!(adu.unit, 1);
158        assert_eq!(adu.fc, 0x03);
159        assert_eq!(adu.data, vec![0x00, 0x00, 0x00, 0x0a]);
160        assert_eq!(adu.transaction, None);
161    }
162
163    #[test]
164    fn test_adu_with_transaction() {
165        let adu = ApplicationDataUnit::new(1, 0x03, vec![]).with_transaction(42);
166        assert_eq!(adu.transaction, Some(42));
167    }
168
169    #[test]
170    fn test_framed_data_unit() {
171        let adu = ApplicationDataUnit::new(1, 0x03, vec![0x00, 0x01]);
172        let raw = vec![0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x01, 0x03, 0x00, 0x01];
173        let frame = FramedDataUnit {
174            adu,
175            raw: raw.clone(),
176        };
177        assert_eq!(frame.raw, raw);
178    }
179
180    #[test]
181    fn test_server_id_single() {
182        let sid = ServerId::single(1, true, vec![1, 2, 3]);
183        assert_eq!(sid.server_id, vec![1]);
184        assert!(sid.run_indicator_status);
185        assert_eq!(sid.additional_data, vec![1, 2, 3]);
186    }
187
188    #[test]
189    fn test_server_id_multi() {
190        let sid = ServerId {
191            server_id: vec![0x01, 0x02, 0x03],
192            run_indicator_status: false,
193            additional_data: vec![0xab, 0xcd],
194        };
195        assert_eq!(sid.server_id.len(), 3);
196        assert!(!sid.run_indicator_status);
197    }
198
199    #[test]
200    fn test_device_object() {
201        let obj = DeviceObject {
202            id: 0x01,
203            value: "ProductCode".to_string(),
204        };
205        assert_eq!(obj.id, 0x01);
206        assert_eq!(obj.value, "ProductCode");
207    }
208
209    #[test]
210    fn test_device_identification() {
211        let di = DeviceIdentification {
212            read_device_id_code: 0x01,
213            conformity_level: 0x81,
214            more_follows: false,
215            next_object_id: 0x00,
216            objects: vec![
217                DeviceObject {
218                    id: 0x00,
219                    value: "VendorName".to_string(),
220                },
221                DeviceObject {
222                    id: 0x01,
223                    value: "ProductCode".to_string(),
224                },
225            ],
226        };
227        assert_eq!(di.read_device_id_code, 0x01);
228        assert_eq!(di.conformity_level, 0x81);
229        assert!(!di.more_follows);
230        assert_eq!(di.objects.len(), 2);
231    }
232
233    #[test]
234    fn test_address_range_default() {
235        let range = AddressRange::default();
236        assert!(range.coils.is_empty());
237        assert!(range.discrete_inputs.is_empty());
238        assert!(range.input_registers.is_empty());
239        assert!(range.holding_registers.is_empty());
240    }
241
242    #[test]
243    fn test_custom_function_code_construction() {
244        let cfc = CustomFunctionCode {
245            fc: 0x65,
246            predict_request_length: Arc::new(|_| CustomFcPredict::Length(8)),
247            predict_response_length: Arc::new(|_| CustomFcPredict::Length(8)),
248            handle: None,
249        };
250        assert_eq!(cfc.fc, 0x65);
251        assert!(cfc.handle.is_none());
252    }
253}