rmodbus/server/
mod.rs

1pub mod context;
2pub mod representable;
3pub mod storage;
4
5use core::slice;
6pub use representable::representations;
7
8#[allow(clippy::wildcard_imports)]
9use crate::consts::*;
10use crate::{calc_crc16, calc_lrc, ErrorKind, ModbusProto, VectorTrait};
11
12/// Modbus frame processor
13///
14/// ```no_run
15/// # #[cfg(feature = "fixedvec")]
16/// # mod with_fixedvec {
17/// use rmodbus::{ModbusFrameBuf, ModbusProto, server::{ModbusFrame, storage::ModbusStorageFull, context::ModbusContext}};
18/// use fixedvec::{FixedVec, alloc_stack}; // for std use regular std::vec::Vec
19///
20/// # fn code() {
21/// let mut ctx = ModbusStorageFull::new();
22///
23/// let unit_id = 1;
24/// loop {
25///     let framebuf:ModbusFrameBuf = [0;256];
26///     // read frame into the buffer
27///     let mut mem = alloc_stack!([u8; 256]);
28///     let mut response = FixedVec::new(&mut mem);
29///     // create new frame processor object
30///     let mut frame = ModbusFrame::new(unit_id, &framebuf, ModbusProto::TcpUdp, &mut response);
31///     // parse frame buffer
32///     if frame.parse().is_ok() {
33///         // parsed ok
34///         if frame.processing_required {
35///             // call a function depending is the request read-only or not
36///             // a little more typing, but allows to lock the context only for reading if writing
37///             // isn't required
38///             let result = match frame.readonly {
39///                 true => frame.process_read(&ctx),
40///                 false => frame.process_write(&mut ctx)
41///             };
42///             if result.is_err() {
43///                 // fn error is returned at this point only if there's no space in the response
44///                 // vec (so can be caused in nostd only)
45///                 continue;
46///             }
47///         }
48///         // processing is over (if required), let's check is the response required
49///         if frame.response_required {
50///             // sets Modbus error if happened, for RTU/ASCII frames adds CRC/LRU
51///             frame.finalize_response();
52///             response.as_slice(); // send response somewhere
53///         }
54///     }
55/// }
56/// # } }
57/// ```
58macro_rules! tcp_response_set_data_len {
59    ($self: expr, $len:expr) => {
60        if $self.proto == ModbusProto::TcpUdp {
61            $self.response.extend(&($len as u16).to_be_bytes())?;
62        }
63    };
64}
65
66#[derive(Debug)]
67#[cfg_attr(feature = "defmt", derive(defmt::Format))]
68pub struct ModbusFrame<'a, V: VectorTrait<u8>> {
69    pub unit_id: u8,
70    buf: &'a [u8],
71    pub response: &'a mut V,
72    pub proto: ModbusProto,
73    /// after parse: is processing required
74    pub processing_required: bool,
75    /// is response required
76    pub response_required: bool,
77    /// which function code to respond to
78    pub responding_to_fn: u8,
79    /// is request read-only
80    pub readonly: bool,
81    /// Modbus frame start in buf (0 for RTU/ASCII, 6 for TCP)
82    pub frame_start: usize,
83    /// function requested
84    pub func: ModbusFunction,
85    /// starting register
86    pub reg: u16,
87    /// registers to process
88    pub count: u16,
89    /// error code
90    pub error: Option<ModbusErrorCode>,
91}
92
93impl<'a, V: VectorTrait<u8>> ModbusFrame<'a, V> {
94    pub fn new(unit_id: u8, buf: &'a [u8], proto: ModbusProto, response: &'a mut V) -> Self {
95        response.clear();
96        Self {
97            unit_id,
98            buf,
99            proto,
100            response,
101            processing_required: false,
102            readonly: true,
103            response_required: false,
104            responding_to_fn: 0,
105            frame_start: 0,
106            count: 1,
107            reg: 0,
108            // default to GetCoils
109            func: ModbusFunction::GetCoils,
110            // simulate invalid starting state with error
111            error: None,
112        }
113    }
114    /// Should be always called if response needs to be sent
115    pub fn finalize_response(&mut self) -> Result<(), ErrorKind> {
116        if let Some(err) = self.error {
117            match self.proto {
118                ModbusProto::TcpUdp => {
119                    self.response
120                        // write 2b length 1b unit ID, 1b function code and 1b error
121                        // 2b transaction ID and 2b protocol ID were already written by .parse()
122                        .extend(&[0, 3, self.unit_id, self.responding_to_fn + 0x80, err.byte()])?;
123                }
124                ModbusProto::Rtu | ModbusProto::Ascii => {
125                    self.response
126                        // write 1b unit ID, 1b function code and 1b error
127                        .extend(&[self.unit_id, self.responding_to_fn + 0x80, err.byte()])?;
128                }
129            }
130        }
131        match self.proto {
132            ModbusProto::Rtu => {
133                let len = self.response.len();
134                if len > u8::MAX as usize {
135                    return Err(ErrorKind::OOB);
136                }
137                #[allow(clippy::cast_possible_truncation)]
138                let crc = calc_crc16(self.response.as_slice(), len as u8);
139                self.response.extend(&crc.to_le_bytes())
140            }
141            ModbusProto::Ascii => {
142                let len = self.response.len();
143                if len > u8::MAX as usize {
144                    return Err(ErrorKind::OOB);
145                }
146                #[allow(clippy::cast_possible_truncation)]
147                let lrc = calc_lrc(self.response.as_slice(), len as u8);
148                self.response.push(lrc)
149            }
150            ModbusProto::TcpUdp => Ok(()),
151        }
152    }
153    /// Process write functions
154    pub fn process_write<C: context::ModbusContext>(
155        &mut self,
156        ctx: &mut C,
157    ) -> Result<(), ErrorKind> {
158        match self.func {
159            ModbusFunction::SetCoil => {
160                // func 5
161                // write single coil
162                if self.buf.len() < self.frame_start + 6 {
163                    return Err(ErrorKind::FrameBroken);
164                }
165                let val = match u16::from_be_bytes([
166                    self.buf[self.frame_start + 4],
167                    self.buf[self.frame_start + 5],
168                ]) {
169                    0xff00 => true,
170                    0x0000 => false,
171                    _ => {
172                        self.error = Some(ModbusErrorCode::IllegalDataValue);
173                        return Ok(());
174                    }
175                };
176                if ctx.set_coil(self.reg, val).is_err() {
177                    self.error = Some(ModbusErrorCode::IllegalDataAddress);
178                    return Ok(());
179                }
180                tcp_response_set_data_len!(self, 6);
181                // 6b unit, func, reg, val
182                self.response
183                    .extend(&self.buf[self.frame_start..self.frame_start + 6])
184            }
185            ModbusFunction::SetHolding => {
186                // func 6
187                // write single register
188                if self.buf.len() < self.frame_start + 6 {
189                    return Err(ErrorKind::FrameBroken);
190                }
191                let val = u16::from_be_bytes([
192                    self.buf[self.frame_start + 4],
193                    self.buf[self.frame_start + 5],
194                ]);
195                if ctx.set_holding(self.reg, val).is_err() {
196                    self.error = Some(ModbusErrorCode::IllegalDataAddress);
197                    return Ok(());
198                }
199                tcp_response_set_data_len!(self, 6);
200                // 6b unit, func, reg, val
201                self.response
202                    .extend(&self.buf[self.frame_start..self.frame_start + 6])
203            }
204            ModbusFunction::SetCoilsBulk | ModbusFunction::SetHoldingsBulk => {
205                // funcs 15 & 16
206                // write multiple coils / registers
207                if self.buf.len() < self.frame_start + 7 {
208                    return Err(ErrorKind::FrameBroken);
209                }
210                let bytes = self.buf[self.frame_start + 6];
211                let result = match self.func {
212                    ModbusFunction::SetCoilsBulk => ctx.set_coils_from_u8(
213                        self.reg,
214                        self.count,
215                        &self.buf[self.frame_start + 7..self.frame_start + 7 + bytes as usize],
216                    ),
217                    ModbusFunction::SetHoldingsBulk => ctx.set_holdings_from_u8(
218                        self.reg,
219                        &self.buf[self.frame_start + 7..self.frame_start + 7 + bytes as usize],
220                    ),
221                    _ => unreachable!("Matched above"),
222                };
223
224                if result.is_ok() {
225                    tcp_response_set_data_len!(self, 6);
226                    // 6b unit, f, reg, cnt
227                    self.response
228                        .extend(&self.buf[self.frame_start..self.frame_start + 6])
229                } else {
230                    self.error = Some(ModbusErrorCode::IllegalDataAddress);
231                    Ok(())
232                }
233            }
234            ModbusFunction::GetHoldings
235            | ModbusFunction::GetInputs
236            | ModbusFunction::GetCoils
237            | ModbusFunction::GetDiscretes => Err(ErrorKind::ReadCallOnWriteFrame),
238        }
239    }
240    /// Construct [`Write`] struct describing the requested write.
241    ///
242    /// If you use this to process the requested write yourself (so not calling
243    /// [`process_write`](ModbusFrame::process_write) with a
244    /// [`ModbusContext`](context::ModbusContext)) don't forget to call
245    /// [`process_external_write`](ModbusFrame::process_external_write), these two calls together
246    /// replace the call to [`process_write`](ModbusFrame::process_write).
247    pub fn get_external_write(&mut self) -> Result<Write<'_>, ErrorKind> {
248        match self.func {
249            ModbusFunction::SetCoil => {
250                // func 5
251                // write single coil
252                let val = match u16::from_be_bytes([
253                    self.buf[self.frame_start + 4],
254                    self.buf[self.frame_start + 5],
255                ]) {
256                    0xff00 => Write::Bits(WriteBits {
257                        address: self.reg,
258                        count: 1,
259                        data: slice::from_ref(&1u8),
260                    }),
261                    0x0000 => Write::Bits(WriteBits {
262                        address: self.reg,
263                        count: 1,
264                        data: slice::from_ref(&0u8),
265                    }),
266                    _ => {
267                        self.set_modbus_error_if_unset(&ErrorKind::IllegalDataValue)?;
268                        return Err(ErrorKind::IllegalDataValue);
269                    }
270                };
271
272                Ok(val)
273            }
274            ModbusFunction::SetHolding => {
275                // func 6
276                // write single register
277
278                let write = Write::Words(WriteWords {
279                    address: self.reg,
280                    count: 1,
281                    data: &self.buf[self.frame_start + 4..self.frame_start + 6],
282                });
283
284                Ok(write)
285            }
286            ModbusFunction::SetCoilsBulk => {
287                // funcs 15 & 16
288                // write multiple coils / registers
289                let bytes = self.buf[self.frame_start + 6];
290                let data_start = self.frame_start + 7;
291
292                let write = Write::Bits(WriteBits {
293                    address: self.reg,
294                    count: self.count,
295                    data: &self.buf[data_start..data_start + bytes as usize],
296                });
297
298                Ok(write)
299            }
300            ModbusFunction::SetHoldingsBulk => {
301                // funcs 15 & 16
302                // write multiple coils / registers
303                let bytes = self.buf[self.frame_start + 6];
304                let data_start = self.frame_start + 7;
305
306                let write = Write::Words(WriteWords {
307                    address: self.reg,
308                    count: self.count,
309                    data: &self.buf[data_start..data_start + bytes as usize],
310                });
311
312                Ok(write)
313            }
314            ModbusFunction::GetHoldings
315            | ModbusFunction::GetInputs
316            | ModbusFunction::GetCoils
317            | ModbusFunction::GetDiscretes => Err(ErrorKind::ReadCallOnWriteFrame),
318        }
319    }
320    /// See [get_external_write](ModbusFrame::get_external_write)
321    pub fn process_external_write(
322        &mut self,
323        write_result: Result<(), ErrorKind>,
324    ) -> Result<(), ErrorKind> {
325        match write_result {
326            Ok(()) => {
327                match self.func {
328                    ModbusFunction::SetCoil
329                    | ModbusFunction::SetHolding
330                    | ModbusFunction::SetCoilsBulk
331                    | ModbusFunction::SetHoldingsBulk => {
332                        // funcs 5 & 6
333                        // write single coil / register
334                        // funcs 15 & 16
335                        // write multiple coils / registers
336
337                        tcp_response_set_data_len!(self, 6);
338                        // 6b unit, func, reg, val
339                        self.response
340                            .extend(&self.buf[self.frame_start..self.frame_start + 6])
341                    }
342                    ModbusFunction::GetHoldings
343                    | ModbusFunction::GetInputs
344                    | ModbusFunction::GetCoils
345                    | ModbusFunction::GetDiscretes => Err(ErrorKind::ReadCallOnWriteFrame),
346                }
347            }
348            Err(e) if e.is_modbus_error() => {
349                self.set_modbus_error_if_unset(&e)?;
350                Ok(())
351            }
352            Err(e) => Err(e),
353        }
354    }
355
356    /// Process read functions
357    #[allow(clippy::manual_is_multiple_of)]
358    pub fn process_read<C: context::ModbusContext>(&mut self, ctx: &C) -> Result<(), ErrorKind> {
359        match self.func {
360            ModbusFunction::GetCoils | ModbusFunction::GetDiscretes => {
361                // funcs 1 - 2
362                // read coils / discretes
363                let mut data_len = self.count >> 3;
364                if self.count % 8 != 0 {
365                    data_len += 1;
366                }
367                tcp_response_set_data_len!(self, data_len + 3);
368                // 2b unit and func
369                self.response
370                    .extend(&self.buf[self.frame_start..self.frame_start + 2])?;
371                if data_len > u16::from(u8::MAX) {
372                    return Err(ErrorKind::OOB);
373                }
374                #[allow(clippy::cast_possible_truncation)]
375                self.response.push(data_len as u8)?;
376                let result = match self.func {
377                    ModbusFunction::GetCoils => {
378                        ctx.get_coils_as_u8(self.reg, self.count, self.response)
379                    }
380                    ModbusFunction::GetDiscretes => {
381                        ctx.get_discretes_as_u8(self.reg, self.count, self.response)
382                    }
383                    _ => unreachable!("Matched above"),
384                };
385                if let Err(e) = result {
386                    if e == ErrorKind::OOBContext {
387                        self.response.cut_end(5, 0);
388                        self.error = Some(ModbusErrorCode::IllegalDataAddress);
389                        Ok(())
390                    } else {
391                        Err(e)
392                    }
393                } else {
394                    Ok(())
395                }
396            }
397            ModbusFunction::GetHoldings | ModbusFunction::GetInputs => {
398                // funcs 3 - 4
399                // read holdings / inputs
400                let data_len = self.count << 1;
401                tcp_response_set_data_len!(self, data_len + 3);
402                // 2b unit and func
403                self.response
404                    .extend(&self.buf[self.frame_start..self.frame_start + 2])?;
405                if data_len > u16::from(u8::MAX) {
406                    return Err(ErrorKind::OOB);
407                }
408                #[allow(clippy::cast_possible_truncation)]
409                // 1b data len
410                self.response.push(data_len as u8)?;
411                let result = match self.func {
412                    ModbusFunction::GetHoldings => {
413                        ctx.get_holdings_as_u8(self.reg, self.count, self.response)
414                    }
415                    ModbusFunction::GetInputs => {
416                        ctx.get_inputs_as_u8(self.reg, self.count, self.response)
417                    }
418                    _ => unreachable!("Matched above"),
419                };
420                if let Err(e) = result {
421                    if e == ErrorKind::OOBContext {
422                        self.response.cut_end(5, 0);
423                        self.error = Some(ModbusErrorCode::IllegalDataAddress);
424                        Ok(())
425                    } else {
426                        Err(e)
427                    }
428                } else {
429                    Ok(())
430                }
431            }
432            ModbusFunction::SetCoil
433            | ModbusFunction::SetHolding
434            | ModbusFunction::SetCoilsBulk
435            | ModbusFunction::SetHoldingsBulk => Err(ErrorKind::WriteCallOnReadFrame),
436        }
437    }
438
439    /// Construct [`Read`] struct describing the requested read.
440    ///
441    /// If you use this to process the requested read yourself (so not calling
442    /// [`process_read`](ModbusFrame::process_read) with a
443    /// [`ModbusContext`](context::ModbusContext)) don't forget to call
444    /// [`process_external_read`](ModbusFrame::process_external_read), these two calls together
445    /// replace the call to [`process_read`](ModbusFrame::process_read).
446    #[allow(clippy::manual_is_multiple_of)]
447    pub fn get_external_read(&mut self) -> Result<Read<'_>, ErrorKind> {
448        match self.func {
449            ModbusFunction::GetCoils | ModbusFunction::GetDiscretes => {
450                // funcs 1 - 2
451                // read coils / discretes
452                let mut data_len = self.count >> 3;
453                if self.count % 8 != 0 {
454                    data_len += 1;
455                }
456                tcp_response_set_data_len!(self, data_len + 3);
457                // 2b unit and func
458                self.response
459                    .extend(&self.buf[self.frame_start..self.frame_start + 2])?;
460                if data_len > u16::from(u8::MAX) {
461                    return Err(ErrorKind::OOB);
462                }
463                #[allow(clippy::cast_possible_truncation)]
464                // 1b data len
465                self.response.push(data_len as u8)?;
466
467                // extend with data_len so we can get the extra space as &mut slice for Read struct
468                let current_length = self.response.len();
469                let new_length = current_length + data_len as usize;
470                self.response.resize(new_length, 0u8)?;
471
472                Ok(Read::Bits(ReadBits {
473                    address: self.reg,
474                    count: self.count,
475                    buf: &mut self.response.as_mut_slice()[current_length..new_length],
476                }))
477            }
478            ModbusFunction::GetHoldings | ModbusFunction::GetInputs => {
479                // funcs 3 - 4
480                // read holdings / inputs
481                let data_len = self.count << 1;
482                tcp_response_set_data_len!(self, data_len + 3);
483                // 2b unit and func
484                self.response
485                    .extend(&self.buf[self.frame_start..self.frame_start + 2])?;
486                if data_len > u16::from(u8::MAX) {
487                    return Err(ErrorKind::OOB);
488                }
489                #[allow(clippy::cast_possible_truncation)]
490                // 1b data len
491                self.response.push(data_len as u8)?;
492
493                // extend with data_len so we can get the extra space as &mut slice for Read struct
494                let current_length = self.response.len();
495                let new_length = current_length + data_len as usize;
496                self.response.resize(new_length, 0u8)?;
497
498                Ok(Read::Words(ReadWords {
499                    address: self.reg,
500                    count: self.count,
501                    buf: &mut self.response.as_mut_slice()[current_length..new_length],
502                }))
503            }
504            ModbusFunction::SetCoil
505            | ModbusFunction::SetHolding
506            | ModbusFunction::SetCoilsBulk
507            | ModbusFunction::SetHoldingsBulk => Err(ErrorKind::WriteCallOnReadFrame),
508        }
509    }
510
511    /// see [get_external_read](ModbusFrame::get_external_read)
512    pub fn process_external_read(
513        &mut self,
514        read_result: Result<(), ErrorKind>,
515    ) -> Result<(), ErrorKind> {
516        match read_result {
517            Ok(()) => match self.func {
518                ModbusFunction::GetCoils
519                | ModbusFunction::GetDiscretes
520                | ModbusFunction::GetHoldings
521                | ModbusFunction::GetInputs => Ok(()),
522                ModbusFunction::SetCoil
523                | ModbusFunction::SetHolding
524                | ModbusFunction::SetCoilsBulk
525                | ModbusFunction::SetHoldingsBulk => Err(ErrorKind::WriteCallOnReadFrame),
526            },
527            Err(e) if e.is_modbus_error() => {
528                self.set_modbus_error_if_unset(&e)?;
529                Ok(())
530            }
531            Err(e) => Err(e),
532        }
533    }
534
535    /// Parse frame buffer
536    #[allow(clippy::too_many_lines)]
537    pub fn parse(&mut self) -> Result<(), ErrorKind> {
538        if self.proto == ModbusProto::TcpUdp {
539            if self.buf.len() < 6 {
540                return Err(ErrorKind::FrameBroken);
541            }
542            //let tr_id = u16::from_be_bytes([self.buf[0], self.buf[1]]);
543            let proto_id = u16::from_be_bytes([self.buf[2], self.buf[3]]);
544            let length = u16::from_be_bytes([self.buf[4], self.buf[5]]);
545            if proto_id != 0 || !(6..=250).contains(&length) {
546                return Err(ErrorKind::FrameBroken);
547            }
548            self.frame_start = 6;
549        }
550        if self.frame_start >= self.buf.len() {
551            return Err(ErrorKind::FrameBroken);
552        }
553        let unit = self.buf[self.frame_start];
554        let broadcast = unit == 0 || unit == 255; // some clients send broadcast to 0xff
555        if !broadcast && unit != self.unit_id {
556            return Ok(());
557        }
558        if !broadcast && self.proto == ModbusProto::TcpUdp {
559            // copy 4 bytes: tr id and proto
560            self.response.extend(&self.buf[0..4])?;
561        }
562        if self.buf.len() < self.frame_start + 2 {
563            return Err(ErrorKind::FrameBroken);
564        }
565
566        // hack since self.func can't represent invalid state
567        self.responding_to_fn = self.buf[self.frame_start + 1];
568        if let Ok(f) = ModbusFunction::try_from(self.buf[self.frame_start + 1]) {
569            self.func = f;
570        } else {
571            // if function is not supported, we still need to return a response
572            // so we set the error code and return
573            if !broadcast {
574                self.response_required = true;
575                self.error = Some(ModbusErrorCode::IllegalFunction);
576            }
577            return Ok(());
578        }
579        macro_rules! check_frame_crc {
580            ($len:expr) => {
581                match self.proto {
582                    ModbusProto::TcpUdp => true,
583                    ModbusProto::Rtu => {
584                        if self.buf.len() < self.frame_start + $len as usize + 2 {
585                            return Err(ErrorKind::FrameBroken);
586                        }
587                        calc_crc16(self.buf, $len)
588                            == u16::from_le_bytes([
589                                self.buf[self.frame_start + $len as usize],
590                                self.buf[self.frame_start + $len as usize + 1],
591                            ])
592                    }
593                    ModbusProto::Ascii => {
594                        if self.buf.len() < self.frame_start + $len as usize + 1 {
595                            return Err(ErrorKind::FrameBroken);
596                        }
597                        calc_lrc(self.buf, $len) == self.buf[self.frame_start + $len as usize]
598                    }
599                }
600            };
601        }
602        match self.func {
603            ModbusFunction::GetCoils | ModbusFunction::GetDiscretes => {
604                // funcs 1 - 2
605                // read coils / discretes
606                if broadcast {
607                    return Ok(());
608                }
609                if self.buf.len() < self.frame_start + 6 {
610                    return Err(ErrorKind::FrameBroken);
611                }
612                if !check_frame_crc!(6) {
613                    return Err(ErrorKind::FrameCRCError);
614                }
615                self.response_required = true;
616                self.count = u16::from_be_bytes([
617                    self.buf[self.frame_start + 4],
618                    self.buf[self.frame_start + 5],
619                ]);
620                if self.count > 2000 {
621                    self.error = Some(ModbusErrorCode::IllegalDataValue);
622                    return Ok(());
623                }
624                self.processing_required = true;
625                self.reg = u16::from_be_bytes([
626                    self.buf[self.frame_start + 2],
627                    self.buf[self.frame_start + 3],
628                ]);
629                Ok(())
630            }
631            ModbusFunction::GetHoldings | ModbusFunction::GetInputs => {
632                // funcs 3 - 4
633                // read holdings / inputs
634                if broadcast {
635                    return Ok(());
636                }
637                if self.buf.len() < self.frame_start + 6 {
638                    return Err(ErrorKind::FrameBroken);
639                }
640                if !check_frame_crc!(6) {
641                    return Err(ErrorKind::FrameCRCError);
642                }
643                self.response_required = true;
644                self.count = u16::from_be_bytes([
645                    self.buf[self.frame_start + 4],
646                    self.buf[self.frame_start + 5],
647                ]);
648                if self.count > 125 {
649                    self.error = Some(ModbusErrorCode::IllegalDataValue);
650                    return Ok(());
651                }
652                self.processing_required = true;
653                self.reg = u16::from_be_bytes([
654                    self.buf[self.frame_start + 2],
655                    self.buf[self.frame_start + 3],
656                ]);
657                Ok(())
658            }
659            ModbusFunction::SetCoil | ModbusFunction::SetHolding => {
660                // func 5 / 6
661                // write single coil / register
662                if self.buf.len() < self.frame_start + 4 {
663                    return Err(ErrorKind::FrameBroken);
664                }
665                if !check_frame_crc!(6) {
666                    return Err(ErrorKind::FrameCRCError);
667                }
668                if !broadcast {
669                    self.response_required = true;
670                }
671                self.count = 1;
672                self.processing_required = true;
673                self.readonly = false;
674                self.reg = u16::from_be_bytes([
675                    self.buf[self.frame_start + 2],
676                    self.buf[self.frame_start + 3],
677                ]);
678                Ok(())
679            }
680            ModbusFunction::SetCoilsBulk | ModbusFunction::SetHoldingsBulk => {
681                // funcs 15 & 16
682                // write multiple coils / registers
683                if self.buf.len() < self.frame_start + 7 {
684                    return Err(ErrorKind::FrameBroken);
685                }
686                let bytes = self.buf[self.frame_start + 6];
687                if !check_frame_crc!(7 + bytes) {
688                    return Err(ErrorKind::FrameCRCError);
689                }
690                if !broadcast {
691                    self.response_required = true;
692                }
693                self.count = u16::from_be_bytes([
694                    self.buf[self.frame_start + 4],
695                    self.buf[self.frame_start + 5],
696                ]);
697                let max_count = match self.func {
698                    ModbusFunction::SetCoilsBulk => 1968,
699                    ModbusFunction::SetHoldingsBulk => 123,
700                    _ => unreachable!("Matched above"),
701                };
702                if self.count > max_count {
703                    self.error = Some(ModbusErrorCode::IllegalDataValue);
704                    return Ok(());
705                }
706                if bytes > 246 {
707                    self.error = Some(ModbusErrorCode::IllegalDataValue);
708                    return Ok(());
709                }
710                self.processing_required = true;
711                self.readonly = false;
712                self.reg = u16::from_be_bytes([
713                    self.buf[self.frame_start + 2],
714                    self.buf[self.frame_start + 3],
715                ]);
716                self.count = u16::from_be_bytes([
717                    self.buf[self.frame_start + 4],
718                    self.buf[self.frame_start + 5],
719                ]);
720                Ok(())
721            }
722        }
723    }
724
725    /// Retrieve which fields of a [`ModbusContext`](`context::ModbusContext`) will be changed by applying this frame
726    ///
727    /// Returns None if no fields will be changed.
728    pub fn changes(&self) -> Option<Changes> {
729        let reg = self.reg;
730        let count = self.count;
731
732        Some(match self.func {
733            ModbusFunction::SetCoil => Changes::Coils { reg, count: 1 },
734            ModbusFunction::SetCoilsBulk => Changes::Coils { reg, count },
735            ModbusFunction::SetHolding => Changes::Holdings { reg, count: 1 },
736            ModbusFunction::SetHoldingsBulk => Changes::Holdings { reg, count },
737            _ => return None,
738        })
739    }
740
741    /// If the error field on the [`ModbusFrame`] isn't already set this function will set it and
742    /// resize the response buffer to what's expected by [`ModbusFrame::finalize_response`]
743    ///
744    /// # Panics
745    ///
746    /// Should not panic
747    pub fn set_modbus_error_if_unset(&mut self, err: &ErrorKind) -> Result<(), ErrorKind> {
748        if self.error.is_none() && err.is_modbus_error() {
749            // leave 0 bytes for RTU/ASCII, leave 4 bytes for TCP/UDP (Transaction ID and Protocol ID)
750            let len_leave_before_finalize = if self.proto == ModbusProto::TcpUdp {
751                4
752            } else {
753                0
754            };
755
756            self.response.resize(len_leave_before_finalize, 0)?;
757            self.error = Some(err.to_modbus_error()?);
758        }
759        Ok(())
760    }
761}
762
763/// See [`ModbusFrame::changes`]
764#[derive(Debug, Copy, Clone, Eq, PartialEq)]
765pub enum Changes {
766    Coils { reg: u16, count: u16 },
767    Holdings { reg: u16, count: u16 },
768}
769
770/// See [`get_external_write`](ModbusFrame::get_external_write)
771#[derive(Debug, Copy, Clone, Eq, PartialEq)]
772pub struct WriteBits<'a> {
773    pub address: u16,
774    pub count: u16,
775    pub data: &'a [u8],
776}
777
778/// See [`get_external_write`](ModbusFrame::get_external_write)
779#[derive(Debug, Copy, Clone, Eq, PartialEq)]
780pub struct WriteWords<'a> {
781    pub address: u16,
782    pub count: u16,
783    pub data: &'a [u8],
784}
785
786/// See [`get_external_write`](ModbusFrame::get_external_write)
787#[derive(Debug, Copy, Clone, Eq, PartialEq)]
788pub enum Write<'a> {
789    Bits(WriteBits<'a>),
790    Words(WriteWords<'a>),
791}
792
793/// See [`get_external_read`](ModbusFrame::get_external_read)
794#[derive(Debug, Eq, PartialEq)]
795pub struct ReadBits<'a> {
796    pub address: u16,
797    pub count: u16,
798    pub buf: &'a mut [u8],
799}
800
801/// See [`get_external_read`](ModbusFrame::get_external_read)
802#[derive(Debug, Eq, PartialEq)]
803pub struct ReadWords<'a> {
804    pub address: u16,
805    pub count: u16,
806    pub buf: &'a mut [u8],
807}
808
809/// See [`get_external_read`](ModbusFrame::get_external_read)
810#[derive(Debug, Eq, PartialEq)]
811pub enum Read<'a> {
812    Bits(ReadBits<'a>),
813    Words(ReadWords<'a>),
814}