pni_sdk/
lib.rs

1/// Centered around the [Get] trait
2pub mod responses;
3
4/// The second byte of a frame
5pub mod command;
6
7/// Configuration + config options
8pub mod config;
9
10/// Acquisition of data
11pub mod acquisition;
12
13/// User + factory device calibration
14pub mod calibration;
15
16use serialport::SerialPort;
17use std::{error::Error, hash::Hasher, string::FromUtf8Error, time::Duration};
18#[macro_use]
19extern crate derive_more;
20
21use command::Command;
22use responses::{Get, ModInfoResp};
23
24
25/// Error that ocurred while reading data back from the device
26#[derive(Debug, Display)]
27pub enum ReadError {
28    /// IO Error when communicating with device on serial port.
29    PipeError(std::io::Error),
30
31    /// Error parsing response/data from device
32    ParseError(String),
33
34    /// Checksum for frame didn't match
35    #[display(
36        fmt = "ChecksumMismatch {{ expected: {}, actual: {} }}",
37        expected,
38        actual
39    )]
40    ChecksumMismatch { expected: u16, actual: u16 }, // in case of misaligned read, return the
41    // actual checksum for easy debugging
42    /// Frame length was different from expected length, check device compatibility or library
43    /// version. Size mismatches result in a PipeError if the frame was shorter than expected
44    /// and a read timed out
45    #[display(fmt = "SizeMismatch {{ expected: {}, actual: {} }}", expected, actual)]
46    SizeMismatch { expected: u16, actual: u16 },
47}
48
49impl Error for ReadError {}
50
51impl From<std::io::Error> for ReadError {
52    fn from(value: std::io::Error) -> Self {
53        Self::PipeError(value)
54    }
55}
56
57impl From<FromUtf8Error> for ReadError {
58    fn from(e: FromUtf8Error) -> Self {
59        Self::ParseError(format!("UTF8 String couldn't be parsed: {}", e))
60    }
61}
62
63/// Error that ocurred while writing data to the device
64#[derive(Debug, Display)]
65pub enum WriteError {
66    /// IO Error when writing to device
67    PipeError(std::io::Error),
68}
69
70impl Error for WriteError {}
71
72impl From<std::io::Error> for WriteError {
73    fn from(value: std::io::Error) -> Self {
74        Self::PipeError(value)
75    }
76}
77
78#[derive(Debug, Display)]
79pub enum RWError {
80    /// Error occurred when reading/parsing data from serial
81    ReadError(ReadError),
82
83    /// Error occurred when writing/serializing data to serial
84    WriteError(WriteError),
85
86    /// Device indicated error status
87    DeviceError(String),
88}
89
90impl Error for RWError {}
91
92impl From<WriteError> for RWError {
93    fn from(value: WriteError) -> Self {
94        Self::WriteError(value)
95    }
96}
97
98impl From<ReadError> for RWError {
99    fn from(value: ReadError) -> Self {
100        Self::ReadError(value)
101    }
102}
103
104/// Represents a connected device
105///
106/// # Examples
107///
108/// ```
109/// # {
110/// use pni_sdk::{Device, acquisition::DataID};
111/// let mut tp3 = pni_sdk::Device::connect(None).expect("Couldn't Auto-Detect connected device");
112/// tp3.set_data_components(vec![DataID::AccelX]);
113/// println!("Accel X: {}", tp3.get_data().unwrap().accel_x.unwrap());
114/// # }
115/// ```
116pub struct Device {
117    serialport: Box<dyn SerialPort>,
118
119    /// Checksum of the current frame so far
120    read_checksum: crc16::State<crc16::XMODEM>,
121
122    /// # of bytes read since the frame started
123    read_bytes: u16,
124}
125
126impl Device {
127    /// Creates a new Device with provided serialport
128    pub fn new(serialport: impl Into<Box<dyn SerialPort>>) -> Self {
129        Self {
130            serialport: serialport.into(),
131            read_checksum: crc16::State::<crc16::XMODEM>::new(),
132            read_bytes: 0,
133        }
134    }
135
136    /// Creates and connects to a device, auto-detecting the serial port, and choosing the
137    /// default baud rate of 38400
138    ///
139    /// # Arguments
140    ///
141    /// * `port` - If [Some], uses the given serial port string. If [None], tries to auto-detect
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// # {
147    /// let tp3 = pni_sdk::Device::connect(None).expect("Auto-Detect connected Device");
148    /// # }
149    /// ```
150    pub fn connect(port: Option<String>) -> Result<Self, Box<dyn Error>> {
151        let ports = serialport::available_ports()?;
152
153        let port = if let Some(provided_port) = port {
154            provided_port
155        } else {
156            match ports.into_iter().fold(None, |chosen, port| {
157                if port.port_name.contains("usb") {
158                    Some(port)
159                } else {
160                    chosen
161                }
162            }) {
163                Some(port) => port.port_name,
164                None => {
165                    return Err(Box::new(serialport::Error::new(
166                        serialport::ErrorKind::NoDevice,
167                        "Could not auto-detect serial port",
168                    )))
169                }
170            }
171        };
172
173        println!("Using port {}", port);
174
175        Ok(Device::new(
176            serialport::new(port, 38400)
177                .data_bits(serialport::DataBits::Eight)
178                .stop_bits(serialport::StopBits::One)
179                .parity(serialport::Parity::None)
180                .timeout(Duration::new(1, 0))
181                .open()?,
182        ))
183    }
184
185    /// Sends the given command and payload to the device, with appropriate CRC and sizing
186    pub fn write_frame(
187        &mut self,
188        command: Command,
189        payload: Option<&[u8]>,
190    ) -> Result<(), WriteError> {
191        let payload_length = if let Some(payload) = payload {
192            payload.len() as u16
193        } else {
194            0
195        };
196
197        // offset of 5 comes from 2 length bytes, 1 command byte, 2 crc bytes
198        let size = (payload_length + 5u16).to_be_bytes();
199        let command = command.discriminant().to_be_bytes();
200
201        // if you are porting this to another language, note the CRC algorithm XMODEM may also be
202        // called CCITT or ITU, but is different from CCITT-FALSE and AUG-CCITT
203        let mut crc = crc16::State::<crc16::XMODEM>::new();
204
205        // write packet size
206        self.serialport.write(&size)?;
207        crc.update(&size);
208
209        // write command
210        self.serialport.write(&command)?;
211        crc.update(&command);
212
213        if let Some(payload_bytes) = payload {
214            // write payload
215            self.serialport.write(payload_bytes)?;
216            crc.update(payload_bytes);
217        }
218
219        // finish and write CRC
220        let crc = &(crc.finish() as u16).to_be_bytes();
221        self.serialport.write(crc)?;
222
223        Ok(())
224    }
225
226    /// Reads, checks then resets checksum when reading a frame.
227    /// Must be called at the end of every frame to reset counters and crc
228    fn end_frame(&mut self, expected_frame_len: u16) -> Result<(), ReadError> {
229        // must compute expected sum before reading the checksum, since reading the checksum
230        // updates the hasher
231        let expected_sum = self.read_checksum.finish() as u16;
232        let checksum: u16 = Get::<u16>::get(self)?;
233
234        // reset checksum (though it should auto-reset to zero...).
235        self.read_checksum = crc16::State::<crc16::XMODEM>::new();
236
237        if expected_sum == checksum && self.read_bytes == expected_frame_len {
238            self.read_bytes = 0;
239            Ok(())
240        } else if self.read_bytes != expected_frame_len {
241            let read_bytes = self.read_bytes;
242            self.read_bytes = 0;
243            Err(ReadError::SizeMismatch {
244                expected: expected_frame_len,
245                actual: read_bytes,
246            })
247        } else {
248            self.read_bytes = 0;
249            Err(ReadError::ChecksumMismatch {
250                expected: expected_sum,
251                actual: checksum,
252            })
253        }
254    }
255
256    /// Returns device type and revision
257    pub fn get_mod_info(&mut self) -> Result<ModInfoResp, RWError> {
258        self.write_frame(Command::GetModInfo, None)?;
259        let expected_size = Get::<u16>::get(self)?;
260        if Get::<u8>::get(self)? == Command::GetModInfoResp.discriminant() {
261            let device_type = Get::<u32>::get_string(self)?;
262            let revision = Get::<u32>::get_string(self)?;
263            self.end_frame(expected_size)?;
264            Ok(ModInfoResp {
265                device_type,
266                revision,
267            })
268        } else {
269            let _ = self.end_frame(expected_size);
270            Err(RWError::ReadError(ReadError::ParseError(
271                "Unexpected response type".to_string(),
272            )))
273        }
274    }
275
276    /// Returns device serial number, which can also be found on the front sticker
277    pub fn serial_number(&mut self) -> Result<u32, RWError> {
278        self.write_frame(Command::SerialNumber, None)?;
279        let expected_size = Get::<u16>::get(self)?;
280        if Get::<u8>::get(self)? == Command::SerialNumberResp.discriminant() {
281            let serial_number = Get::<u32>::get(self)?;
282            self.end_frame(expected_size)?;
283            Ok(serial_number)
284        } else {
285            let _ = self.end_frame(expected_size);
286            Err(RWError::ReadError(ReadError::ParseError(
287                "Unexpected response type".to_string(),
288            )))
289        }
290    }
291
292    /// This frame commands the device to save internal configurations and user calibration to non-volatile memory. Internal configurations and user calibration are restored on power up. The frame has no payload. This is the ONLY command that causes the device to save information to non-volatile memory.
293    /// See also: [Device::get_config], [Device::set_config]
294    pub fn save(&mut self) -> Result<(), RWError> {
295        self.write_frame(Command::Save, None)?;
296
297        let expected_size = Get::<u16>::get(self)?;
298        if Get::<u8>::get(self)? == Command::SaveDone.discriminant() {
299            let error_code = Get::<u16>::get(self)?;
300            self.end_frame(expected_size)?;
301            if error_code != 0 {
302                return Err(RWError::DeviceError(
303                    "Recieved error code from device, settings not saved succesfully".to_string(),
304                ));
305            }
306            Ok(())
307        } else {
308            let _ = self.end_frame(expected_size);
309            Err(RWError::ReadError(ReadError::ParseError(
310                "Unexpected response type".to_string(),
311            )))
312        }
313    }
314
315    /// "Powers up" the device by sending data over serial (asks for SerialPort) Consumes the power up packet emitted by the device, useful to call after you call
316    /// power_down and reconnect the device
317    pub fn power_up(&mut self) -> Result<(), RWError> {
318        self.write_frame(Command::SerialNumber, None)?;
319
320        let expected_size = Get::<u16>::get(self)?;
321        let resp_command = Get::<u8>::get(self)?;
322
323        if resp_command == Command::PowerUpDone.discriminant() {
324            self.end_frame(expected_size)?;
325            Ok(())
326        } else if resp_command == Command::SerialNumberResp.discriminant() {
327            // if the device is already powered up or if it did buffering of the wake-up command,
328            // we might actually get the serial number back!
329            Get::<u32>::get(self)?;
330            self.end_frame(expected_size)?;
331            Ok(())
332        } else {
333            let _ = self.end_frame(expected_size);
334            Err(RWError::ReadError(ReadError::ParseError(
335                "Unexpected response type".to_string(),
336            )))
337        }
338    }
339
340    /// This frame is used to power-down the module. The frame has no payload. The command will power down all peripherals including the sensors, microprocessor, and RS-232 driver. However, the driver chip has a feature to keep the Rx line enabled. The device will power up when it receives any signal on the native UART Rx line.
341    /// This frame frequently does not recieve a response even when it works, it's suggested that
342    /// you ignore ParseErrors
343    fn power_down_impl(&mut self) -> Result<(), RWError> {
344        self.write_frame(Command::PowerDown, None)?;
345
346        let expected_size = Get::<u16>::get(self)?;
347        if Get::<u8>::get(self)? == Command::PowerDownDone.discriminant() {
348            self.end_frame(expected_size)?;
349            Ok(())
350        } else {
351            let _ = self.end_frame(expected_size);
352            Err(RWError::ReadError(ReadError::ParseError(
353                "Unexpected response type".to_string(),
354            )))
355        }
356    }
357    
358    /// You should consider using [Self::power_down] instead of [Self::power_down_raw] to avoid
359    /// weird serialport behavior
360    ///
361    /// This frame is used to power-down the module. The frame has no payload. The command will power down all peripherals including the sensors, microprocessor, and RS-232 driver. However, the driver chip has a feature to keep the Rx line enabled. The device will power up when it receives any signal on the native UART Rx line.
362    /// This frame frequently does not recieve a response even when it works, it's suggested that
363    /// you ignore ParseErrors
364    #[cfg(feature = "reserved")]
365    pub fn power_down_raw(&mut self) -> Result<(), RWError> {
366        self.power_down_impl()
367    }
368
369    //NOTE: when powering up, we want to connect to the same device in case multiple devices were
370    //provided? Otherwise we basically force the end user to deliberately re-choose the new device
371    //anyhow by re-constructing tp3. Consuming self in power down also drops the serial port which
372    //is desireable
373    /// This frame is used to power-down the module. The frame has no payload. The command will power down all peripherals including the sensors, microprocessor, and RS-232 driver. However, the driver chip has a feature to keep the Rx line enabled. The device will power up when it receives any signal on the native UART Rx line.
374    /// Similar to power_down_raw, but ignores common errors due to power down, and takes ownership to hang up the socket and force developer to create a new tp3 object
375    /// The very action of reconnecting the device will cause it to power back up.
376    pub fn power_down(mut self) -> Result<(), RWError> {
377        let ret = match self.power_down_impl() {
378            Ok(_) => Ok(()),
379            Err(RWError::ReadError(_)) => Ok(()),
380            Err(e) => Err(e),
381        };
382        ret
383    }
384}
385
386// NOTE: when testing or writing doctests, be sure to put everything in its own scope so that the
387// serialport is dropped afte each test
388#[cfg(test)]
389mod tests {
390    use crate::acquisition::*;
391    use crate::*;
392
393    #[test]
394    fn continuous_mode() {
395        let tp3 = Device::connect(None).expect("connects to device");
396        let mut tp3 = tp3
397            .continuous_mode_easy(0.25, vec![DataID::AccelX])
398            .expect("got into cont mode");
399        {
400            let mut iter = tp3.iter();
401            for _ in 0..16 {
402                assert!(match iter.next() { Some(Ok(Data { accel_x: Some(_accel_measurement), ..})) => true, _ => false }, "Calling next on interator in continuous mode should yield the data we asked for");
403            }
404        }
405
406        let mut tp3 = tp3.stop_continuous_mode_easy().unwrap();
407        {
408            let mut iter = tp3.iter();
409            assert!(
410                match iter.next() {
411                    None => true,
412                    _ => false,
413                },
414                "Stop continious mode should leave continuous mode"
415            )
416        }
417    }
418}