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 into_data<Request>(self, request: &Frame<Request>) -> Result<T::Data, ValidationError>
67 where
68 T: Response<Request>,
69 {
70 if self.calculate_crc() != self.crc.get() {
71 return Err(ValidationError::Crc(CrcError));
72 }
73
74 if self.unit_id != request.unit_id {
75 debug!(
76 "Request unit id 0x{:02X} does not match response unit id 0x{:02X}",
77 request.unit_id, self.unit_id
78 );
79 return Err(ValidationError::UnexpectedResponse);
80 }
81
82 if !self.pdu.matches_request(&request.pdu) {
83 debug!("Request PDU does not match response PDU");
84 return Err(ValidationError::UnexpectedResponse);
85 }
86
87 Ok(self.pdu.into_data())
88 }
89
90 pub fn view(&self) -> FrameView<'_>
92 where
93 T: IntoBytes + Unaligned + Immutable,
94 {
95 FrameView {
96 buf: self.as_bytes(),
97 }
98 }
99}
100
101#[derive(Debug)]
103pub struct FrameBuilder<T> {
104 inner: Frame<T>,
105}
106
107impl<T: Pdu> FrameBuilder<T> {
108 pub const fn with_pdu(unit_id: u8, pdu: T) -> Self {
113 Self {
114 inner: Frame::without_crc(unit_id, pdu),
116 }
117 }
118
119 pub const fn new(unit_id: u8) -> Self {
121 Self::with_pdu(unit_id, T::DEFAULT)
122 }
123
124 pub const fn set_unit_id(&mut self, unit_id: u8) {
126 self.inner.unit_id = unit_id;
127 }
128
129 pub fn build_ref(&mut self) -> &mut Frame<T> {
132 self.inner.update_crc();
133 &mut self.inner
134 }
135
136 pub fn build(mut self) -> Frame<T> {
138 self.inner.update_crc();
139 self.inner
140 }
141
142 pub fn pdu(&self) -> &T {
144 &self.inner.pdu
145 }
146
147 pub fn pdu_mut(&mut self) -> &mut T {
149 &mut self.inner.pdu
150 }
151}
152
153impl<T: Pdu> Default for FrameBuilder<T> {
154 fn default() -> Self {
155 Self::new(0)
156 }
157}
158
159pub struct FrameView<'a> {
170 buf: &'a [u8],
171}
172
173impl<'a> FrameView<'a> {
174 pub fn try_from_bytes(buf: &'a [u8]) -> Option<Self> {
177 if (4..=256).contains(&buf.len()) {
178 Some(Self { buf })
179 } else {
180 None
181 }
182 }
183
184 pub const fn unit_id(&self) -> u8 {
186 self.buf[0]
187 }
188
189 pub const fn crc(&self) -> u16 {
192 u16::from_le_bytes([self.buf[self.buf.len() - 2], self.buf[self.buf.len() - 1]])
193 }
194
195 fn calculate_crc(&self) -> u16 {
196 MODBUS_CRC.checksum(&self.buf[..self.buf.len() - 2])
197 }
198
199 pub fn pdu(self) -> Result<&'a PduView, CrcError> {
201 self.validate_crc()?;
202 Ok(
203 PduView::ref_from_bytes(&self.buf[1..self.buf.len() - 2])
204 .expect("PduView is Unaligned"),
205 )
206 }
207
208 pub fn validate_crc(&self) -> Result<(), CrcError> {
214 if self.calculate_crc() == self.crc() {
215 Ok(())
216 } else {
217 Err(CrcError)
218 }
219 }
220}
221
222#[derive(Debug, FromBytes, KnownLayout, Immutable, IntoBytes, Unaligned)]
224#[repr(C)]
225pub struct PduView {
226 pub function_code: u8,
228 pub data: [u8],
230}
231
232impl PduView {
233 #[inline]
251 pub fn parse<T: Pdu>(&self) -> Option<&T> {
252 if self.function_code == T::FUNCTION_CODE {
253 T::try_ref_from_bytes(self.as_bytes()).ok()
254 } else {
255 None
256 }
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use hex_literal::hex;
263
264 use super::*;
265
266 #[test]
267 fn test_length_validation() {
268 assert!(FrameView::try_from_bytes(&hex!()).is_none());
269 assert!(FrameView::try_from_bytes(&hex!("00 00 00")).is_none());
270 assert!(FrameView::try_from_bytes(&hex!("00 00 00 00")).is_some());
271 assert!(FrameView::try_from_bytes(&[0; 256]).is_some());
272 assert!(FrameView::try_from_bytes(&[0; 257]).is_none());
273 }
274}