ssp_server/
mock.rs

1use std::io::{Read, Write};
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::sync::Arc;
4use std::time;
5
6use serialport::{SerialPort, TTYPort};
7
8use ssp::{ResponseOps, Result};
9
10use super::device_handle::{BAUD_RATE, SERIAL_TIMEOUT_MS};
11
12/// Represents a mock SSP device used for integration tests.
13///
14/// The device can be configured to respond with a default message, e.g. for polling.
15///
16/// Messages generated by the device are sent encoded as SSP formatted message over a serial
17/// connection.
18pub struct MockDevice {
19    serial_path: String,
20    serial_port: TTYPort,
21    msg_type: ssp::MessageType,
22    response_status: ssp::ResponseStatus,
23}
24
25impl MockDevice {
26    /// Creates a new [MockDevice].
27    ///
28    /// # Parameters
29    ///
30    /// - `path`: file path to the serial device, e.g. `/dev/ttyUSB0`
31    /// - `msg_type`: SSP [MessageType](ssp::MessageType) for default replies
32    /// - `response_status`: default SSP [ResponseStatus](ssp::ResponseStatus) for default replies
33    pub fn new(
34        path: &str,
35        msg_type: ssp::MessageType,
36        response_status: ssp::ResponseStatus,
37    ) -> Result<Self> {
38        let serial_port = serialport::new(path, BAUD_RATE)
39            // disable flow control serial lines
40            .flow_control(serialport::FlowControl::None)
41            // eight-bit data size
42            .data_bits(serialport::DataBits::Eight)
43            // no control bit parity
44            .parity(serialport::Parity::None)
45            // two bit stop
46            .stop_bits(serialport::StopBits::Two)
47            // serial device times out after 10 seconds, so do we
48            .timeout(time::Duration::from_millis(SERIAL_TIMEOUT_MS))
49            // get back a TTY port for POSIX systems, Windows is not supported
50            .open_native()?;
51
52        Ok(Self {
53            serial_path: path.into(),
54            serial_port,
55            msg_type,
56            response_status,
57        })
58    }
59
60    /// Gets a reference to the file path to the serial device.
61    pub fn serial_path(&self) -> &str {
62        self.serial_path.as_str()
63    }
64
65    /// Gets the default [MessageType](ssp::MessageType).
66    pub const fn msg_type(&self) -> ssp::MessageType {
67        self.msg_type
68    }
69
70    /// Sets the default [MessageType](ssp::MessageType).
71    pub fn set_msg_type(&mut self, msg_type: ssp::MessageType) {
72        self.msg_type = msg_type;
73    }
74
75    /// Gets the default [ResponseStatus](ssp::ResponseStatus).
76    pub const fn response_status(&self) -> ssp::ResponseStatus {
77        self.response_status
78    }
79
80    /// Sets the default [ResponseStatus](ssp::ResponseStatus).
81    pub fn set_response_status(&mut self, response_status: ssp::ResponseStatus) {
82        self.response_status = response_status;
83    }
84
85    /// Gets a default response message based on the [MessageType](ssp::MessageType).
86    pub fn default_response(msg_type: ssp::MessageType) -> Option<Box<dyn ResponseOps>> {
87        match msg_type {
88            ssp::MessageType::SetInhibits => Some(Box::new(ssp::SetInhibitsResponse::new())),
89            ssp::MessageType::DisplayOn => Some(Box::new(ssp::DisplayOnResponse::new())),
90            ssp::MessageType::DisplayOff => Some(Box::new(ssp::DisplayOffResponse::new())),
91            ssp::MessageType::SetupRequest => Some(Box::new(ssp::SetupRequestResponse::new())),
92            ssp::MessageType::HostProtocolVersion => {
93                Some(Box::new(ssp::HostProtocolVersionResponse::new()))
94            }
95            ssp::MessageType::Poll => Some(Box::new(ssp::PollResponse::new())),
96            ssp::MessageType::Reject => Some(Box::new(ssp::RejectResponse::new())),
97            ssp::MessageType::Disable => Some(Box::new(ssp::DisableResponse::new())),
98            ssp::MessageType::Enable => Some(Box::new(ssp::EnableResponse::new())),
99            ssp::MessageType::SerialNumber => Some(Box::new(ssp::SerialNumberResponse::new())),
100            ssp::MessageType::UnitData => Some(Box::new(ssp::UnitDataResponse::new())),
101            ssp::MessageType::ChannelValueData => {
102                Some(Box::new(ssp::ChannelValueDataResponse::new()))
103            }
104            ssp::MessageType::Synchronisation => Some(Box::new(ssp::PollResponse::new())),
105            ssp::MessageType::LastRejectCode => Some(Box::new(ssp::LastRejectCodeResponse::new())),
106            ssp::MessageType::Hold => Some(Box::new(ssp::HoldResponse::new())),
107            ssp::MessageType::DatasetVersion => Some(Box::new(ssp::DatasetVersionResponse::new())),
108            ssp::MessageType::SetBarcodeReaderConfiguration => {
109                Some(Box::new(ssp::SetBarcodeReaderConfigurationResponse::new()))
110            }
111            ssp::MessageType::GetBarcodeReaderConfiguration => {
112                Some(Box::new(ssp::GetBarcodeReaderConfigurationResponse::new()))
113            }
114            ssp::MessageType::GetBarcodeInhibit => {
115                Some(Box::new(ssp::GetBarcodeInhibitResponse::new()))
116            }
117            ssp::MessageType::SetBarcodeInhibit => {
118                Some(Box::new(ssp::SetBarcodeInhibitResponse::new()))
119            }
120            ssp::MessageType::GetBarcodeData => Some(Box::new(ssp::GetBarcodeDataResponse::new())),
121            ssp::MessageType::Empty => Some(Box::new(ssp::EmptyResponse::new())),
122            ssp::MessageType::PayoutByDenomination => {
123                Some(Box::new(ssp::PayoutByDenominationResponse::new()))
124            }
125            ssp::MessageType::SmartEmpty => Some(Box::new(ssp::SmartEmptyResponse::new())),
126            ssp::MessageType::ConfigureBezel => Some(Box::new(ssp::ConfigureBezelResponse::new())),
127            ssp::MessageType::PollWithAck => Some(Box::new(ssp::PollWithAckResponse::new())),
128            ssp::MessageType::EventAck => Some(Box::new(ssp::EventAckResponse::new())),
129            ssp::MessageType::DisablePayout => Some(Box::new(ssp::DisablePayoutResponse::new())),
130            ssp::MessageType::EnablePayout => Some(Box::new(ssp::EnablePayoutResponse::new())),
131            // FIXME: add support for encrypted message types...
132            _ => None,
133        }
134    }
135
136    /// Get the response data length.
137    pub fn response_data_len(msg_type: ssp::MessageType) -> usize {
138        match msg_type {
139            ssp::MessageType::SetInhibits => ssp::len::SET_INHIBITS_RESPONSE,
140            ssp::MessageType::DisplayOn => ssp::len::DISPLAY_ON_RESPONSE,
141            ssp::MessageType::DisplayOff => ssp::len::DISPLAY_OFF_RESPONSE,
142            ssp::MessageType::SetupRequest => ssp::len::SETUP_REQUEST_RESPONSE,
143            ssp::MessageType::HostProtocolVersion => ssp::len::HOST_PROTOCOL_VERSION_RESPONSE,
144            ssp::MessageType::Poll => ssp::len::POLL_RESPONSE,
145            ssp::MessageType::Reject => ssp::len::REJECT_RESPONSE,
146            ssp::MessageType::Disable => ssp::len::DISABLE_RESPONSE,
147            ssp::MessageType::Enable => ssp::len::ENABLE_RESPONSE,
148            ssp::MessageType::SerialNumber => ssp::len::SERIAL_NUMBER_RESPONSE,
149            ssp::MessageType::UnitData => ssp::len::UNIT_DATA_RESPONSE,
150            ssp::MessageType::ChannelValueData => ssp::len::CHANNEL_VALUE_DATA_RESPONSE,
151            ssp::MessageType::Synchronisation => ssp::len::SYNC_RESPONSE,
152            ssp::MessageType::LastRejectCode => ssp::len::LAST_REJECT_CODE_RESPONSE,
153            ssp::MessageType::Hold => ssp::len::HOLD_RESPONSE,
154            ssp::MessageType::DatasetVersion => ssp::len::DATASET_VERSION_RESPONSE,
155            ssp::MessageType::SetBarcodeReaderConfiguration => {
156                ssp::len::SET_BARCODE_READER_CONFIGURATION_RESPONSE
157            }
158            ssp::MessageType::GetBarcodeReaderConfiguration => {
159                ssp::len::GET_BARCODE_READER_CONFIGURATION_RESPONSE
160            }
161            ssp::MessageType::GetBarcodeInhibit => ssp::len::GET_BARCODE_INHIBIT_RESPONSE,
162            ssp::MessageType::SetBarcodeInhibit => ssp::len::SET_BARCODE_INHIBIT_RESPONSE,
163            ssp::MessageType::GetBarcodeData => ssp::len::GET_BARCODE_DATA_RESPONSE,
164            ssp::MessageType::Empty => ssp::len::EMPTY_RESPONSE,
165            ssp::MessageType::PayoutByDenomination => ssp::len::PAYOUT_BY_DENOMINATION_RESPONSE,
166            ssp::MessageType::SmartEmpty => ssp::len::SMART_EMPTY_RESPONSE,
167            ssp::MessageType::ConfigureBezel => ssp::len::CONFIGURE_BEZEL_RESPONSE,
168            ssp::MessageType::PollWithAck => ssp::len::POLL_WITH_ACK_RESPONSE,
169            ssp::MessageType::EventAck => ssp::len::EVENT_ACK_RESPONSE,
170            ssp::MessageType::DisablePayout => ssp::len::DISABLE_PAYOUT_RESPONSE,
171            ssp::MessageType::EnablePayout => ssp::len::ENABLE_PAYOUT_RESPONSE,
172            // FIXME: add support for encrypted message types...
173            _ => 1,
174        }
175    }
176
177    /// Sends a default response message over the serial connection.
178    pub fn send_default_response(&mut self) -> Result<()> {
179        if let Some(mut res) = Self::default_response(self.msg_type) {
180            res.set_response_status(self.response_status);
181            self.serial_port.write_all(res.as_bytes())?;
182        }
183        Ok(())
184    }
185
186    /// Starts a device server loop
187    ///
188    /// The loop will continue serving configured responses to received message until the `stop`
189    /// flag is set to true.
190    ///
191    /// # Parameters
192    ///
193    /// - `stop`: atomic flag for stopping the device server loop.
194    pub fn serve(&mut self, stop: Arc<AtomicBool>) -> Result<()> {
195        let mut buf = [0u8; ssp::len::MAX_MESSAGE];
196        while !stop.load(Ordering::Relaxed) {
197            if self.serial_port.bytes_to_read().unwrap_or(0) > 0
198                && self
199                    .serial_port
200                    .read_exact(&mut buf[..=ssp::message::index::LEN])
201                    .is_ok()
202            {
203                log::debug!("Received host message");
204                let len = buf[ssp::message::index::LEN] as usize;
205                let rem = len + ssp::message::index::LEN + 2;
206
207                self.serial_port
208                    .read_exact(&mut buf[ssp::message::index::LEN..rem])?;
209
210                log::debug!("Receive message bytes: {:x?}", &buf[..rem]);
211
212                let msg_type: ssp::MessageType = if len > ssp::message::index::DATA {
213                    buf[ssp::message::index::DATA].into()
214                } else {
215                    self.msg_type
216                };
217
218                let seq_id = !ssp::SequenceId::from(buf[ssp::message::index::SEQ_ID]);
219
220                if let Some(mut res) = Self::default_response(msg_type) {
221                    res.set_response_status(self.response_status);
222                    res.set_data_len(Self::response_data_len(msg_type) as u8);
223                    res.set_sequence_id(seq_id);
224                    log::debug!("Writing response: {:x?}", res.as_bytes());
225                    self.serial_port.write_all(res.as_bytes())?;
226                    log::trace!("Successfully wrote response");
227                    self.serial_port.flush()?;
228                }
229            }
230            buf.iter_mut().for_each(|b| *b = 0);
231        }
232        Ok(())
233    }
234}