1use defmt_or_log::*;
2use zerocopy::{FromBytes, Immutable, IntoBytes, Unaligned, little_endian};
3use zerocopy_derive::*;
4
5use crate::{
6 Pdu,
7 pdu::{CrcError, Response, ValidationError},
8};
9
10const MODBUS_CRC: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_MODBUS);
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoBytes, Unaligned, Immutable, FromBytes)]
14#[repr(C)]
15pub struct Frame<T> {
16 unit_id: u8,
17 pdu: T,
18 crc: little_endian::U16,
19}
20
21impl<T> Frame<T> {
22 pub fn new(unit_id: u8, pdu: T) -> Self
25 where
26 T: Pdu,
27 {
28 let mut frame = Self::without_crc(unit_id, pdu);
29 frame.update_crc();
30 frame
31 }
32
33 const fn without_crc(unit_id: u8, pdu: T) -> Self {
34 Frame {
35 unit_id,
36 pdu,
37 crc: little_endian::U16::ZERO,
38 }
39 }
40
41 pub const fn builder(unit_id: u8) -> FrameBuilder<T>
43 where
44 T: Pdu,
45 {
46 FrameBuilder::new(unit_id)
47 }
48
49 fn calculate_crc(&self) -> u16
50 where
51 T: IntoBytes + Unaligned + Immutable,
52 {
53 let bytes = self.as_bytes();
54 MODBUS_CRC.checksum(&bytes[..bytes.len() - 2])
56 }
57
58 fn update_crc(&mut self)
59 where
60 T: IntoBytes + Unaligned + Immutable,
61 {
62 self.crc = self.calculate_crc().into();
63 }
64
65 pub fn validate_crc(&self) -> Result<(), CrcError>
69 where
70 T: IntoBytes + Unaligned + Immutable,
71 {
72 if self.calculate_crc() == self.crc.get() {
73 Ok(())
74 } else {
75 Err(CrcError)
76 }
77 }
78
79 pub fn unit_id(&self) -> u8 {
81 self.unit_id
82 }
83
84 pub fn pdu(&self) -> &T {
86 &self.pdu
87 }
88
89 pub fn into_data<Request>(self, request: &Frame<Request>) -> Result<T::Data, ValidationError>
91 where
92 T: Response<Request>,
93 {
94 self.validate_crc()?;
95
96 if self.unit_id != request.unit_id {
97 debug!(
98 "Request unit id 0x{:02X} does not match response unit id 0x{:02X}",
99 request.unit_id, self.unit_id
100 );
101 return Err(ValidationError::UnexpectedResponse);
102 }
103
104 if !self.pdu.matches_request(&request.pdu) {
105 debug!("Request PDU does not match response PDU");
106 return Err(ValidationError::UnexpectedResponse);
107 }
108
109 Ok(self.pdu.into_data())
110 }
111
112 pub fn view(&self) -> FrameView<'_>
114 where
115 T: IntoBytes + Unaligned + Immutable,
116 {
117 FrameView {
118 buf: self.as_bytes(),
119 }
120 }
121}
122
123#[derive(Debug)]
125pub struct FrameBuilder<T> {
126 inner: Frame<T>,
127}
128
129impl<T: Pdu> FrameBuilder<T> {
130 pub const fn with_pdu(unit_id: u8, pdu: T) -> Self {
135 Self {
136 inner: Frame::without_crc(unit_id, pdu),
138 }
139 }
140
141 pub const fn new(unit_id: u8) -> Self {
143 Self::with_pdu(unit_id, T::DEFAULT)
144 }
145
146 pub const fn set_unit_id(&mut self, unit_id: u8) {
148 self.inner.unit_id = unit_id;
149 }
150
151 pub fn build_ref(&mut self) -> &mut Frame<T> {
154 self.inner.update_crc();
155 &mut self.inner
156 }
157
158 pub fn build(mut self) -> Frame<T> {
160 self.inner.update_crc();
161 self.inner
162 }
163
164 pub fn pdu(&self) -> &T {
166 &self.inner.pdu
167 }
168
169 pub fn pdu_mut(&mut self) -> &mut T {
171 &mut self.inner.pdu
172 }
173}
174
175impl<T: Pdu> Default for FrameBuilder<T> {
176 fn default() -> Self {
177 Self::new(0)
178 }
179}
180
181pub struct FrameView<'a> {
192 buf: &'a [u8],
193}
194
195impl<'a> FrameView<'a> {
196 pub fn try_from_bytes(buf: &'a [u8]) -> Option<Self> {
199 if (4..=256).contains(&buf.len()) {
200 Some(Self { buf })
201 } else {
202 None
203 }
204 }
205
206 pub const fn unit_id(&self) -> u8 {
208 self.buf[0]
209 }
210
211 pub const fn crc(&self) -> u16 {
214 u16::from_le_bytes([self.buf[self.buf.len() - 2], self.buf[self.buf.len() - 1]])
215 }
216
217 fn calculate_crc(&self) -> u16 {
218 MODBUS_CRC.checksum(&self.buf[..self.buf.len() - 2])
219 }
220
221 pub fn pdu(self) -> Result<&'a PduView, CrcError> {
223 self.validate_crc()?;
224 Ok(
225 PduView::ref_from_bytes(&self.buf[1..self.buf.len() - 2])
226 .expect("PduView is Unaligned"),
227 )
228 }
229
230 pub fn validate_crc(&self) -> Result<(), CrcError> {
236 if self.calculate_crc() == self.crc() {
237 Ok(())
238 } else {
239 Err(CrcError)
240 }
241 }
242}
243
244#[derive(Debug, FromBytes, KnownLayout, Immutable, IntoBytes, Unaligned)]
246#[repr(C)]
247pub struct PduView {
248 pub function_code: u8,
250 pub data: [u8],
252}
253
254impl PduView {
255 #[inline]
273 pub fn parse<T: Pdu>(&self) -> Option<&T> {
274 if self.function_code == T::FUNCTION_CODE {
275 T::try_ref_from_bytes(self.as_bytes()).ok()
276 } else {
277 None
278 }
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use hex_literal::hex;
285
286 use super::*;
287
288 #[test]
289 fn test_length_validation() {
290 assert!(FrameView::try_from_bytes(&hex!()).is_none());
291 assert!(FrameView::try_from_bytes(&hex!("00 00 00")).is_none());
292 assert!(FrameView::try_from_bytes(&hex!("00 00 00 00")).is_some());
293 assert!(FrameView::try_from_bytes(&[0; 256]).is_some());
294 assert!(FrameView::try_from_bytes(&[0; 257]).is_none());
295 }
296}