Skip to main content

rfid_silion_compat/
client.rs

1use core::fmt;
2
3use crate::codes::StatusCode;
4use crate::error::ProtocolError;
5use crate::frame::{ReaderFrame, build_host_frame, parse_reader_frame};
6use crate::transport::ReaderTransport;
7
8/// Errors produced by the protocol client.
9#[derive(Debug)]
10pub enum ClientError<TE> {
11    /// Underlying transport error.
12    Transport(TE),
13    /// Packet or payload format error.
14    Protocol(ProtocolError),
15    /// Reader returned a non-success status code.
16    ReaderStatus {
17        /// Raw status code.
18        status_raw: u16,
19        /// Parsed status when known.
20        status: Option<StatusCode>,
21    },
22    /// Response command code does not match request command code.
23    UnexpectedResponseCommand {
24        /// Sent command code.
25        expected: u8,
26        /// Received command code.
27        actual: u8,
28    },
29}
30
31impl<TE: fmt::Display> fmt::Display for ClientError<TE> {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            Self::Transport(e) => write!(f, "transport error: {e}"),
35            Self::Protocol(e) => write!(f, "protocol error: {e}"),
36            Self::ReaderStatus { status_raw, status } => {
37                write!(f, "reader returned status 0x{status_raw:04X} ({status:?})")
38            }
39            Self::UnexpectedResponseCommand { expected, actual } => {
40                write!(
41                    f,
42                    "unexpected response command: expected 0x{expected:02X}, got 0x{actual:02X}"
43                )
44            }
45        }
46    }
47}
48
49impl<TE: fmt::Debug + fmt::Display> std::error::Error for ClientError<TE> {}
50
51/// Protocol client over a [`ReaderTransport`].
52pub struct ReaderClient<T: ReaderTransport> {
53    transport: T,
54}
55
56impl<T: ReaderTransport> ReaderClient<T> {
57    /// Create a low-level protocol client over a transport.
58    pub fn new(transport: T) -> Self {
59        Self { transport }
60    }
61
62    /// Consume this client and return the wrapped transport.
63    pub fn into_inner(self) -> T {
64        self.transport
65    }
66
67    /// Return a mutable reference to the wrapped transport.
68    pub fn transport_mut(&mut self) -> &mut T {
69        &mut self.transport
70    }
71
72    /// Write a pre-built host frame to the transport without reading a response.
73    pub async fn write_frame(&mut self, data: &[u8]) -> Result<(), ClientError<T::Error>> {
74        self.transport
75            .write_all(data)
76            .await
77            .map_err(ClientError::Transport)
78    }
79
80    /// Read and parse one reader-to-host frame without sending a request first.
81    pub async fn read_frame(&mut self) -> Result<ReaderFrame, ClientError<T::Error>> {
82        let mut prefix = [0u8; 5];
83        self.transport
84            .read_exact(&mut prefix)
85            .await
86            .map_err(ClientError::Transport)?;
87
88        let data_len = prefix[1] as usize;
89        let mut suffix = vec![0u8; data_len + 2];
90        self.transport
91            .read_exact(&mut suffix)
92            .await
93            .map_err(ClientError::Transport)?;
94
95        let mut packet = Vec::with_capacity(prefix.len() + suffix.len());
96        packet.extend_from_slice(&prefix);
97        packet.extend_from_slice(&suffix);
98
99        parse_reader_frame(&packet).map_err(ClientError::Protocol)
100    }
101
102    /// Send one pre-built host frame and parse a single response frame.
103    pub async fn transact_frame(
104        &mut self,
105        request: &[u8],
106    ) -> Result<ReaderFrame, ClientError<T::Error>> {
107        self.transport
108            .write_all(request)
109            .await
110            .map_err(ClientError::Transport)?;
111        self.read_frame().await
112    }
113
114    /// Build and send one command payload, then parse one validated response.
115    pub async fn transact(
116        &mut self,
117        command: u8,
118        data: &[u8],
119    ) -> Result<ReaderFrame, ClientError<T::Error>> {
120        let request = build_host_frame(command, data).map_err(ClientError::Protocol)?;
121        let response = self.transact_frame(&request).await?;
122
123        if response.command != command {
124            return Err(ClientError::UnexpectedResponseCommand {
125                expected: command,
126                actual: response.command,
127            });
128        }
129
130        if response.status_raw != StatusCode::Success as u16 {
131            return Err(ClientError::ReaderStatus {
132                status_raw: response.status_raw,
133                status: response.status,
134            });
135        }
136
137        Ok(response)
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::ReaderClient;
144    use crate::client::ClientError;
145    use crate::codes::CommandCode;
146    use crate::test_support::{MockInteraction, MockTransport};
147
148    #[test]
149    fn transact_success() {
150        let transport = MockTransport::scripted(vec![MockInteraction {
151            request_command: CommandCode::GetCurrentRegion as u8,
152            response_status: 0x0000,
153            response_data: vec![0x01],
154        }]);
155
156        let mut client = ReaderClient::new(transport);
157        let frame =
158            futures::executor::block_on(client.transact(CommandCode::GetCurrentRegion as u8, &[]))
159                .expect("transact should succeed");
160        assert_eq!(frame.command, CommandCode::GetCurrentRegion as u8);
161        assert_eq!(frame.data, vec![0x01]);
162    }
163
164    #[test]
165    fn transact_reader_status_error() {
166        let transport = MockTransport::scripted(vec![MockInteraction {
167            request_command: CommandCode::GetCurrentRegion as u8,
168            response_status: 0x010B,
169            response_data: vec![],
170        }]);
171
172        let mut client = ReaderClient::new(transport);
173        let err =
174            futures::executor::block_on(client.transact(CommandCode::GetCurrentRegion as u8, &[]))
175                .expect_err("transact should fail with reader status");
176
177        match err {
178            ClientError::ReaderStatus { status_raw, .. } => assert_eq!(status_raw, 0x010B),
179            other => panic!("unexpected error: {other:?}"),
180        }
181    }
182}