Skip to main content

rusty_modbus_server/store/
mod.rs

1//! Data store abstraction for the four Modbus data tables, plus optional
2//! file-record, FIFO-queue, and serial-line-diagnostic capabilities.
3
4mod bits;
5pub mod memory;
6
7use std::future::Future;
8
9use rusty_modbus_types::{
10    DiagnosticSubFunction, ExceptionCode, MAX_FIFO_VALUES, MAX_PDU_SIZE, MAX_READ_COILS,
11    MAX_READ_DISCRETE_INPUTS, MAX_READ_REGISTERS, MAX_WRITE_COILS, MAX_WRITE_REGISTERS,
12};
13
14pub(crate) const MAX_FILE_RECORD_REGISTERS: usize = 122;
15pub(crate) const MAX_COMM_EVENT_LOG_EVENTS: usize = 64;
16pub(crate) const MAX_DIAGNOSTIC_RESPONSE_DATA_LEN: usize = MAX_PDU_SIZE - 3;
17pub(crate) const MAX_SERVER_ID_BYTES: usize = MAX_PDU_SIZE - 2;
18
19/// Fixed fields returned with a Get Comm Event Log response (FC 0x0C).
20///
21/// Event bytes are carried separately by append-style datastore hooks so
22/// direct-access stores can write them into the final response buffer.
23#[derive(Debug, Clone, Copy, Default)]
24pub struct CommEventLogMeta {
25    /// Status word: `0x0000` ready, `0xFFFF` busy.
26    pub status: u16,
27    /// Event counter value.
28    pub event_count: u16,
29    /// Message counter value.
30    pub message_count: u16,
31}
32
33/// Snapshot of the communications event log returned by Get Comm Event Log
34/// (FC 0x0C, Spec V1.1b3 §6.10).
35///
36/// Owned (rather than borrowing the codec response type) so a [`DataStore`] can
37/// build it behind its async boundary; the handler derives the wire `byte_count`
38/// as `events.len() + 6`.
39#[derive(Debug, Clone, Default)]
40pub struct CommEventLog {
41    /// Status word: `0x0000` ready, `0xFFFF` busy.
42    pub status: u16,
43    /// Event counter value.
44    pub event_count: u16,
45    /// Message counter value.
46    pub message_count: u16,
47    /// Event bytes (0..=64).
48    pub events: Vec<u8>,
49}
50
51pub(crate) fn unpack_packed_coils(
52    quantity: u16,
53    packed_values: &[u8],
54    out: &mut [bool],
55) -> Result<usize, ExceptionCode> {
56    let quantity = validate_packed_coils(quantity, packed_values)?;
57    if out.len() < quantity {
58        return Err(ExceptionCode::IllegalDataValue);
59    }
60    for (byte_index, &byte) in packed_values.iter().enumerate() {
61        let start = byte_index * 8;
62        let bit_count = (quantity - start).min(8);
63        for bit in 0..bit_count {
64            out[start + bit] = (byte >> bit) & 1 == 1;
65        }
66    }
67    Ok(quantity)
68}
69
70pub(crate) fn unpack_register_values_be(
71    quantity: u16,
72    value_bytes: &[u8],
73    out: &mut [u16],
74) -> Result<usize, ExceptionCode> {
75    let quantity = validate_register_values_be(quantity, value_bytes)?;
76    if out.len() < quantity {
77        return Err(ExceptionCode::IllegalDataValue);
78    }
79    for (slot, chunk) in out
80        .iter_mut()
81        .zip(value_bytes.chunks_exact(2))
82        .take(quantity)
83    {
84        *slot = u16::from_be_bytes([chunk[0], chunk[1]]);
85    }
86    Ok(quantity)
87}
88
89pub(crate) fn validate_packed_coils(
90    quantity: u16,
91    packed_values: &[u8],
92) -> Result<usize, ExceptionCode> {
93    if quantity == 0 || quantity > MAX_WRITE_COILS {
94        return Err(ExceptionCode::IllegalDataValue);
95    }
96    let expected = usize::from(quantity).div_ceil(8);
97    if packed_values.len() != expected {
98        return Err(ExceptionCode::IllegalDataValue);
99    }
100    Ok(usize::from(quantity))
101}
102
103pub(crate) fn validate_register_values_be(
104    quantity: u16,
105    value_bytes: &[u8],
106) -> Result<usize, ExceptionCode> {
107    if quantity == 0 || quantity > MAX_WRITE_REGISTERS {
108        return Err(ExceptionCode::IllegalDataValue);
109    }
110    let expected = usize::from(quantity) * 2;
111    if value_bytes.len() != expected {
112        return Err(ExceptionCode::IllegalDataValue);
113    }
114    Ok(usize::from(quantity))
115}
116
117pub(crate) fn pack_coils(bits: &[bool], out: &mut [u8]) -> Result<(), ExceptionCode> {
118    let byte_count = bits.len().div_ceil(8);
119    if out.len() < byte_count {
120        return Err(ExceptionCode::IllegalDataValue);
121    }
122    for (byte_index, out_byte) in out[..byte_count].iter_mut().enumerate() {
123        let start = byte_index * 8;
124        let end = (start + 8).min(bits.len());
125        let mut byte = 0u8;
126        for (bit, &value) in bits[start..end].iter().enumerate() {
127            byte |= u8::from(value) << bit;
128        }
129        *out_byte = byte;
130    }
131    Ok(())
132}
133
134pub(crate) fn pack_registers_be(registers: &[u16], out: &mut [u8]) -> Result<(), ExceptionCode> {
135    let byte_count = registers.len() * 2;
136    if out.len() < byte_count {
137        return Err(ExceptionCode::IllegalDataValue);
138    }
139    for (chunk, &value) in out[..byte_count].chunks_exact_mut(2).zip(registers) {
140        chunk.copy_from_slice(&value.to_be_bytes());
141    }
142    Ok(())
143}
144
145/// Async trait abstracting the four Modbus data tables (Spec V1.1b3 §4.3).
146///
147/// All methods are async to support database-backed and remote-proxied stores.
148/// Return types use `impl Future<...> + Send` to ensure compatibility with
149/// `tokio::spawn` in the server runtime.
150///
151/// Read methods take `&mut [T]` buffers to avoid heap allocation per request.
152///
153/// The eight methods covering the four core data tables (coils, discrete inputs,
154/// holding/input registers) are **required**. The remaining methods — file
155/// records, FIFO queues, and the serial-line diagnostics family — are
156/// **optional**: each has a default body returning the spec-correct exception
157/// for an unsupported capability, so existing implementations keep compiling and
158/// only override the capabilities they actually serve.
159pub trait DataStore: Send + Sync {
160    // ── Coils (read-write bits) ────────────────────────────────────
161
162    /// Read coil statuses into `buf`. Returns number of coils written.
163    ///
164    /// # Errors
165    ///
166    /// Returns `IllegalDataAddress` if `address + quantity` exceeds the address space.
167    fn read_coils(
168        &self,
169        address: u16,
170        quantity: u16,
171        buf: &mut [bool],
172    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send;
173
174    /// Read coil statuses directly into the Modbus packed-bit wire format.
175    ///
176    /// The default implementation delegates to [`Self::read_coils`] through a
177    /// bounded scratch buffer. Stores with direct table access can override this
178    /// method to avoid the intermediate bool slice and response repacking.
179    fn read_coils_packed(
180        &self,
181        address: u16,
182        quantity: u16,
183        out: &mut [u8],
184    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
185        async move {
186            let mut values = [false; MAX_READ_COILS as usize];
187            let count = self.read_coils(address, quantity, &mut values).await?;
188            if count > values.len() || count > usize::from(quantity) {
189                return Err(ExceptionCode::ServerDeviceFailure);
190            }
191            pack_coils(&values[..count], out)?;
192            Ok(count)
193        }
194    }
195
196    /// Write a single coil.
197    fn write_coil(
198        &self,
199        address: u16,
200        value: bool,
201    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send;
202
203    /// Write multiple coils.
204    fn write_coils(
205        &self,
206        address: u16,
207        values: &[bool],
208    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send;
209
210    /// Write multiple coils from the Modbus packed-bit wire representation.
211    ///
212    /// The default implementation unpacks into a bounded stack buffer and then
213    /// delegates to [`Self::write_coils`]. Stores with direct table access can
214    /// override this method to avoid the intermediate bool slice entirely.
215    fn write_coils_packed(
216        &self,
217        address: u16,
218        quantity: u16,
219        packed_values: &[u8],
220    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send {
221        async move {
222            let mut values = [false; MAX_WRITE_COILS as usize];
223            let quantity = unpack_packed_coils(quantity, packed_values, &mut values)?;
224            self.write_coils(address, &values[..quantity]).await
225        }
226    }
227
228    // ── Discrete Inputs (read-only bits) ───────────────────────────
229
230    /// Read discrete input statuses into `buf`.
231    fn read_discrete_inputs(
232        &self,
233        address: u16,
234        quantity: u16,
235        buf: &mut [bool],
236    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send;
237
238    /// Read discrete input statuses directly into the Modbus packed-bit wire format.
239    ///
240    /// The default implementation delegates to [`Self::read_discrete_inputs`]
241    /// through a bounded scratch buffer. Stores with direct table access can
242    /// override this method to avoid the intermediate bool slice.
243    fn read_discrete_inputs_packed(
244        &self,
245        address: u16,
246        quantity: u16,
247        out: &mut [u8],
248    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
249        async move {
250            let mut values = [false; MAX_READ_DISCRETE_INPUTS as usize];
251            let count = self
252                .read_discrete_inputs(address, quantity, &mut values)
253                .await?;
254            if count > values.len() || count > usize::from(quantity) {
255                return Err(ExceptionCode::ServerDeviceFailure);
256            }
257            pack_coils(&values[..count], out)?;
258            Ok(count)
259        }
260    }
261
262    // ── Holding Registers (read-write words) ───────────────────────
263
264    /// Read holding registers into `buf`. Returns number of registers written.
265    fn read_holding_registers(
266        &self,
267        address: u16,
268        quantity: u16,
269        buf: &mut [u16],
270    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send;
271
272    /// Read holding registers directly into big-endian Modbus wire bytes.
273    ///
274    /// The default implementation delegates to [`Self::read_holding_registers`]
275    /// through a bounded scratch buffer. Stores with direct table access can
276    /// override this method to avoid the intermediate register slice and
277    /// response encoding pass.
278    fn read_holding_registers_be(
279        &self,
280        address: u16,
281        quantity: u16,
282        out: &mut [u8],
283    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
284        async move {
285            let mut values = [0u16; MAX_READ_REGISTERS as usize];
286            let count = self
287                .read_holding_registers(address, quantity, &mut values)
288                .await?;
289            if count > values.len() || count > usize::from(quantity) {
290                return Err(ExceptionCode::ServerDeviceFailure);
291            }
292            pack_registers_be(&values[..count], out)?;
293            Ok(count)
294        }
295    }
296
297    /// Write a single holding register.
298    fn write_register(
299        &self,
300        address: u16,
301        value: u16,
302    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send;
303
304    /// Write multiple holding registers.
305    fn write_registers(
306        &self,
307        address: u16,
308        values: &[u16],
309    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send;
310
311    /// Write multiple holding registers from big-endian Modbus wire bytes.
312    ///
313    /// The default implementation unpacks into a bounded stack buffer and then
314    /// delegates to [`Self::write_registers`]. Stores with direct table access
315    /// can override this method to avoid the intermediate register slice.
316    fn write_registers_be(
317        &self,
318        address: u16,
319        quantity: u16,
320        value_bytes: &[u8],
321    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send {
322        async move {
323            let mut values = [0u16; MAX_WRITE_REGISTERS as usize];
324            let quantity = unpack_register_values_be(quantity, value_bytes, &mut values)?;
325            self.write_registers(address, &values[..quantity]).await
326        }
327    }
328
329    // ── Input Registers (read-only words) ──────────────────────────
330
331    /// Read input registers into `buf`.
332    fn read_input_registers(
333        &self,
334        address: u16,
335        quantity: u16,
336        buf: &mut [u16],
337    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send;
338
339    /// Read input registers directly into big-endian Modbus wire bytes.
340    ///
341    /// The default implementation delegates to [`Self::read_input_registers`]
342    /// through a bounded scratch buffer. Stores with direct table access can
343    /// override this method to avoid the intermediate register slice.
344    fn read_input_registers_be(
345        &self,
346        address: u16,
347        quantity: u16,
348        out: &mut [u8],
349    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
350        async move {
351            let mut values = [0u16; MAX_READ_REGISTERS as usize];
352            let count = self
353                .read_input_registers(address, quantity, &mut values)
354                .await?;
355            if count > values.len() || count > usize::from(quantity) {
356                return Err(ExceptionCode::ServerDeviceFailure);
357            }
358            pack_registers_be(&values[..count], out)?;
359            Ok(count)
360        }
361    }
362
363    // ── File Records (FC 0x14 / 0x15) — optional capability ────────
364
365    /// Read one file sub-record (`record_length` registers from `record_number`
366    /// in file `file_number`) into `buf`; returns the number of registers
367    /// written (Spec V1.1b3 §6.14).
368    ///
369    /// The default returns [`ExceptionCode::IllegalFunction`] — a store that
370    /// does not maintain file records reports `0x01` for FC 0x14.
371    fn read_file_record(
372        &self,
373        file_number: u16,
374        record_number: u16,
375        record_length: u16,
376        buf: &mut [u16],
377    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
378        async move {
379            let _ = (file_number, record_number, record_length, buf);
380            Err(ExceptionCode::IllegalFunction)
381        }
382    }
383
384    /// Read one file sub-record directly into big-endian Modbus wire bytes.
385    ///
386    /// The default implementation delegates to [`Self::read_file_record`] with
387    /// a bounded scratch buffer and then packs that scratch into `out`. Stores
388    /// with direct file access can override this method to avoid the
389    /// intermediate register slice.
390    fn read_file_record_be(
391        &self,
392        file_number: u16,
393        record_number: u16,
394        record_length: u16,
395        out: &mut [u8],
396    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
397        async move {
398            let mut values = [0u16; MAX_FILE_RECORD_REGISTERS];
399            let count = self
400                .read_file_record(file_number, record_number, record_length, &mut values)
401                .await?;
402            if count > values.len() || count > usize::from(record_length) {
403                return Err(ExceptionCode::ServerDeviceFailure);
404            }
405            pack_registers_be(&values[..count], out)?;
406            Ok(count)
407        }
408    }
409
410    /// Write `values` to `record_number` in file `file_number` (Spec V1.1b3 §6.15).
411    ///
412    /// The default returns [`ExceptionCode::IllegalFunction`].
413    fn write_file_record(
414        &self,
415        file_number: u16,
416        record_number: u16,
417        values: &[u16],
418    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send {
419        async move {
420            let _ = (file_number, record_number, values);
421            Err(ExceptionCode::IllegalFunction)
422        }
423    }
424
425    /// Write one file sub-record from big-endian Modbus wire bytes.
426    ///
427    /// The default implementation unpacks the wire bytes and delegates to
428    /// [`Self::write_file_record`]. Stores with direct file access can override
429    /// this method to avoid the intermediate register vector.
430    fn write_file_record_be(
431        &self,
432        file_number: u16,
433        record_number: u16,
434        record_length: u16,
435        value_bytes: &[u8],
436    ) -> impl Future<Output = Result<(), ExceptionCode>> + Send {
437        async move {
438            let expected = usize::from(record_length) * 2;
439            if value_bytes.len() != expected {
440                return Err(ExceptionCode::IllegalDataValue);
441            }
442            let values: Vec<u16> = value_bytes
443                .chunks_exact(2)
444                .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
445                .collect();
446            self.write_file_record(file_number, record_number, &values)
447                .await
448        }
449    }
450
451    // ── FIFO Queue (FC 0x18) — optional capability ─────────────────
452
453    /// Return a non-destructive snapshot of the FIFO queue whose pointer is at
454    /// `address` (at most 31 values; Spec V1.1b3 §6.18 — reading the queue MUST
455    /// NOT drain it).
456    ///
457    /// The default returns [`ExceptionCode::IllegalDataAddress`] — there is no
458    /// FIFO at the requested address (Figure 28).
459    fn read_fifo_queue(
460        &self,
461        address: u16,
462    ) -> impl Future<Output = Result<Vec<u16>, ExceptionCode>> + Send {
463        async move {
464            let _ = address;
465            Err(ExceptionCode::IllegalDataAddress)
466        }
467    }
468
469    /// Read a FIFO queue snapshot directly into big-endian Modbus wire bytes.
470    ///
471    /// The default implementation delegates to [`Self::read_fifo_queue`] and
472    /// packs the returned values. Stores with direct queue access can override
473    /// this method to avoid cloning the queue and allocating an intermediate
474    /// byte buffer.
475    fn read_fifo_queue_be(
476        &self,
477        address: u16,
478        out: &mut [u8],
479    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
480        async move {
481            let values = self.read_fifo_queue(address).await?;
482            if values.len() > usize::from(MAX_FIFO_VALUES) {
483                return Err(ExceptionCode::IllegalDataValue);
484            }
485            pack_registers_be(&values, out)?;
486            Ok(values.len())
487        }
488    }
489
490    // ── Serial-line diagnostics (FC 0x07/0x08/0x0B/0x0C/0x11) ───────
491
492    /// Read the eight device-specific exception-status coils as one byte
493    /// (FC 0x07, §6.7). The default returns [`ExceptionCode::IllegalFunction`].
494    fn read_exception_status(&self) -> impl Future<Output = Result<u8, ExceptionCode>> + Send {
495        async { Err(ExceptionCode::IllegalFunction) }
496    }
497
498    /// Get the comm event counter as `(status, event_count)` (FC 0x0B, §6.9).
499    /// The default returns [`ExceptionCode::IllegalFunction`].
500    fn get_comm_event_counter(
501        &self,
502    ) -> impl Future<Output = Result<(u16, u16), ExceptionCode>> + Send {
503        async { Err(ExceptionCode::IllegalFunction) }
504    }
505
506    /// Get the communications event log (FC 0x0C, §6.10).
507    /// The default returns [`ExceptionCode::IllegalFunction`].
508    fn get_comm_event_log(
509        &self,
510    ) -> impl Future<Output = Result<CommEventLog, ExceptionCode>> + Send {
511        async { Err(ExceptionCode::IllegalFunction) }
512    }
513
514    /// Append FC 0x0C communication event bytes to `out`.
515    ///
516    /// The default delegates to [`Self::get_comm_event_log`]. Direct-access
517    /// stores can override this method to avoid materializing the event list in
518    /// an intermediate `Vec` before response construction.
519    fn append_comm_event_log(
520        &self,
521        out: &mut Vec<u8>,
522    ) -> impl Future<Output = Result<CommEventLogMeta, ExceptionCode>> + Send {
523        async move {
524            let log = self.get_comm_event_log().await?;
525            if log.events.len() > MAX_COMM_EVENT_LOG_EVENTS {
526                return Err(ExceptionCode::ServerDeviceFailure);
527            }
528            out.extend_from_slice(&log.events);
529            Ok(CommEventLogMeta {
530                status: log.status,
531                event_count: log.event_count,
532                message_count: log.message_count,
533            })
534        }
535    }
536
537    /// Report a device-specific server-identification blob (FC 0x11, §6.13).
538    ///
539    /// The returned bytes become the response's `data` field (the handler
540    /// prepends the byte count). The default returns
541    /// [`ExceptionCode::IllegalFunction`].
542    fn report_server_id(&self) -> impl Future<Output = Result<Vec<u8>, ExceptionCode>> + Send {
543        async { Err(ExceptionCode::IllegalFunction) }
544    }
545
546    /// Append the FC 0x11 server-identification data bytes to `out`.
547    ///
548    /// The default delegates to [`Self::report_server_id`]. Direct-access
549    /// stores can override this method to avoid cloning the identification blob
550    /// before the handler copies it into the final response PDU.
551    fn append_server_id(
552        &self,
553        out: &mut Vec<u8>,
554    ) -> impl Future<Output = Result<usize, ExceptionCode>> + Send {
555        async move {
556            let data = self.report_server_id().await?;
557            if data.len() > MAX_SERVER_ID_BYTES {
558                return Err(ExceptionCode::ServerDeviceFailure);
559            }
560            out.extend_from_slice(&data);
561            Ok(data.len())
562        }
563    }
564
565    /// Execute a Diagnostics sub-function (FC 0x08, §6.8).
566    ///
567    /// `Ok(Some(data))` echoes `data` back in the response; `Ok(None)`
568    /// suppresses the response entirely (per the spec, e.g. Force Listen Only
569    /// Mode); `Err` returns an exception.
570    ///
571    /// The default loops back Return Query Data (0x0000) and reports every other
572    /// sub-function as [`ExceptionCode::IllegalFunction`] (Figure 18: an
573    /// unsupported sub-function is an illegal *function*, not an illegal data
574    /// value).
575    fn diagnostic(
576        &self,
577        sub_function: DiagnosticSubFunction,
578        data: &[u8],
579    ) -> impl Future<Output = Result<Option<Vec<u8>>, ExceptionCode>> + Send {
580        async move {
581            match sub_function {
582                DiagnosticSubFunction::ReturnQueryData => Ok(Some(data.to_vec())),
583                _ => Err(ExceptionCode::IllegalFunction),
584            }
585        }
586    }
587
588    /// Append Diagnostics response data bytes to `out`.
589    ///
590    /// The default delegates to [`Self::diagnostic`]. Stores that can produce a
591    /// response from borrowed request bytes can override this method to avoid an
592    /// intermediate owned `Vec` before the handler builds the final response PDU.
593    fn append_diagnostic_response(
594        &self,
595        sub_function: DiagnosticSubFunction,
596        data: &[u8],
597        out: &mut Vec<u8>,
598    ) -> impl Future<Output = Result<Option<usize>, ExceptionCode>> + Send {
599        async move {
600            let Some(data) = self.diagnostic(sub_function, data).await? else {
601                return Ok(None);
602            };
603            if data.len() > MAX_DIAGNOSTIC_RESPONSE_DATA_LEN {
604                return Err(ExceptionCode::ServerDeviceFailure);
605            }
606            out.extend_from_slice(&data);
607            Ok(Some(data.len()))
608        }
609    }
610}