Documentation and organization improvements
This commit is contained in:
		
							parent
							
								
									2a43bf94bd
								
							
						
					
					
						commit
						1b4ccd0dde
					
				
					 5 changed files with 131 additions and 49 deletions
				
			
		|  | @ -3,7 +3,7 @@ name = "obd2" | ||||||
| description = "Utility for reading data from a vehicle over OBD-II" | description = "Utility for reading data from a vehicle over OBD-II" | ||||||
| license = "MIT OR Apache-2.0" | license = "MIT OR Apache-2.0" | ||||||
| repository = "https://github.com/rsammelson/obd2" | repository = "https://github.com/rsammelson/obd2" | ||||||
| version = "0.1.0" | version = "0.2.0-pre1" | ||||||
| edition = "2021" | edition = "2021" | ||||||
| 
 | 
 | ||||||
| [dependencies] | [dependencies] | ||||||
|  |  | ||||||
|  | @ -2,14 +2,30 @@ use core::fmt; | ||||||
| 
 | 
 | ||||||
| pub type Result<T> = std::result::Result<T, Error>; | pub type Result<T> = std::result::Result<T, Error>; | ||||||
| 
 | 
 | ||||||
|  | /// A higher-level API for using an OBD-II device
 | ||||||
| pub trait Obd2Device { | pub trait Obd2Device { | ||||||
|     /// Send an OBD command with mode and PID, and get a list of responses (one for each ECU that
 |     /// Send an OBD-II command with mode and PID and get responses
 | ||||||
|     /// responds)
 |     ///
 | ||||||
|  |     /// The responses are a list with one element for each ECU that responds. The data is decoded
 | ||||||
|  |     /// into the ODB-II bytes from the vehicle and the first two bytes of the
 | ||||||
|  |     /// response---representing the mode and PID the vehicle received---are validated and removed.
 | ||||||
|     fn obd_command(&mut self, mode: u8, pid: u8) -> Result<Vec<Vec<u8>>>; |     fn obd_command(&mut self, mode: u8, pid: u8) -> Result<Vec<Vec<u8>>>; | ||||||
| 
 | 
 | ||||||
|     /// Like [obd_command](Self::obd_command), but for commands that do not require a PID
 |     /// Send an OBD-II command with only mode and get responses
 | ||||||
|  |     ///
 | ||||||
|  |     /// The responses are a list with one element for each ECU that responds. The data is decoded
 | ||||||
|  |     /// into the ODB-II bytes from the vehicle and the first byte of the response---representing
 | ||||||
|  |     /// the mode the vehicle recieved---is validated and removed.
 | ||||||
|     fn obd_mode_command(&mut self, mode: u8) -> Result<Vec<Vec<u8>>>; |     fn obd_mode_command(&mut self, mode: u8) -> Result<Vec<Vec<u8>>>; | ||||||
| 
 | 
 | ||||||
|  |     /// Send command and get list of OBD-II responses as an array
 | ||||||
|  |     ///
 | ||||||
|  |     /// Like [obd_command](Self::obd_command), but each ECU's response (after removing the first
 | ||||||
|  |     /// two bytes) is converted to an array of the specified length. If any response is the wrong
 | ||||||
|  |     /// length, and error is returned.
 | ||||||
|  |     ///
 | ||||||
|  |     /// This function can be used when the response length is known, so that it is easier to index
 | ||||||
|  |     /// into the response without causing a panic and without dealing with Options.
 | ||||||
|     fn obd_command_len<const RESPONSE_LENGTH: usize>( |     fn obd_command_len<const RESPONSE_LENGTH: usize>( | ||||||
|         &mut self, |         &mut self, | ||||||
|         mode: u8, |         mode: u8, | ||||||
|  | @ -25,6 +41,12 @@ pub trait Obd2Device { | ||||||
|             .collect() |             .collect() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Send command and get array of OBD-II responses with each as an array
 | ||||||
|  |     ///
 | ||||||
|  |     /// Like [obd_command_len](Self::obd_command_len), but also convert the list of ECU responses
 | ||||||
|  |     /// to an array. This can be used when the number of ECUs that should respond is known in
 | ||||||
|  |     /// advance. Most commonly, this will be when the count of ECUs is one, for values where only a
 | ||||||
|  |     /// single ECU should respond like the speed of the vehicle.
 | ||||||
|     fn obd_command_cnt_len<const RESPONSE_COUNT: usize, const RESPONSE_LENGTH: usize>( |     fn obd_command_cnt_len<const RESPONSE_COUNT: usize, const RESPONSE_LENGTH: usize>( | ||||||
|         &mut self, |         &mut self, | ||||||
|         mode: u8, |         mode: u8, | ||||||
|  | @ -37,8 +59,10 @@ pub trait Obd2Device { | ||||||
|             .map_err(|_| Error::IncorrectResponseLength("count", RESPONSE_COUNT, count)) |             .map_err(|_| Error::IncorrectResponseLength("count", RESPONSE_COUNT, count)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Retreive the VIN (vehicle identification number), this should match the one printed on the
 |     /// Retreive the VIN (vehicle identification number)
 | ||||||
|     /// vehicle
 |     ///
 | ||||||
|  |     /// This should match the number printed on the vehicle, and is a good command for checking
 | ||||||
|  |     /// that the OBD-II interface is working correctly.
 | ||||||
|     fn get_vin(&mut self) -> Result<String> { |     fn get_vin(&mut self) -> Result<String> { | ||||||
|         let mut result = self.obd_command(0x09, 0x02)?.pop().unwrap(); |         let mut result = self.obd_command(0x09, 0x02)?.pop().unwrap(); | ||||||
|         result.remove(0); // do not know what this byte is
 |         result.remove(0); // do not know what this byte is
 | ||||||
|  |  | ||||||
|  | @ -7,6 +7,12 @@ use std::{ | ||||||
| 
 | 
 | ||||||
| use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; | use super::{Error, Obd2BaseDevice, Obd2Reader, Result}; | ||||||
| 
 | 
 | ||||||
|  | /// An ELM327 OBD-II adapter
 | ||||||
|  | ///
 | ||||||
|  | /// It communicates with the computer over UART using an FTDI FT232R USB-to-UART converter.
 | ||||||
|  | /// Commands to the device itself are indicated by sending "AT" followed by the command, while
 | ||||||
|  | /// plain strings of hex data indicate OBD-II requests to be sent to the vehicle. The responses of
 | ||||||
|  | /// the vehicle are echoed back as hex characters. Capitalization and spaces are always ignored.
 | ||||||
| pub struct Elm327 { | pub struct Elm327 { | ||||||
|     device: ftdi::Device, |     device: ftdi::Device, | ||||||
|     buffer: VecDeque<u8>, |     buffer: VecDeque<u8>, | ||||||
|  | @ -28,23 +34,16 @@ impl Obd2BaseDevice for Elm327 { | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn flush(&mut self) -> Result<()> { |     fn send_cmd(&mut self, data: &[u8]) -> Result<()> { | ||||||
|         thread::sleep(time::Duration::from_millis(500)); |         self.device.write_all(data)?; | ||||||
|         self.read_into_queue()?; |  | ||||||
|         self.buffer.clear(); |  | ||||||
|         thread::sleep(time::Duration::from_millis(500)); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn send_serial_cmd(&mut self, data: &str) -> Result<()> { |  | ||||||
|         self.device.write_all(data.as_bytes())?; |  | ||||||
|         self.device.write_all(b"\r\n")?; |         self.device.write_all(b"\r\n")?; | ||||||
|  |         // thread::sleep(time::Duration::from_millis(200));
 | ||||||
|         let line = self.get_line()?; |         let line = self.get_line()?; | ||||||
|         if line.as_ref().is_some_and(|v| v == data.as_bytes()) { |         if line.as_ref().is_some_and(|v| v == data) { | ||||||
|             Ok(()) |             Ok(()) | ||||||
|         } else { |         } else { | ||||||
|             Err(Error::Communication(format!( |             Err(Error::Communication(format!( | ||||||
|                 "send_serial_cmd: got {:?} instead of echoed command ({})", |                 "send_serial_cmd: got {:?} instead of echoed command ({:?})", | ||||||
|                 line, data |                 line, data | ||||||
|             ))) |             ))) | ||||||
|         } |         } | ||||||
|  | @ -56,7 +55,13 @@ impl Obd2Reader for Elm327 { | ||||||
|         self.get_until(b'\n', false) |         self.get_until(b'\n', false) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn get_until_prompt(&mut self) -> Result<Option<Vec<u8>>> { |     /// Read data until the ELM327's prompt character is printed
 | ||||||
|  |     ///
 | ||||||
|  |     /// This will recieve the entire OBD-II response. The prompt signifies that the ELM327 is ready
 | ||||||
|  |     /// for another command. If this is not called after each OBD-II command is sent, the prompt
 | ||||||
|  |     /// character will come out of the recieve queue later and because it is not valid hex this
 | ||||||
|  |     /// could cause problems. If a timeout occurs, `Ok(None)` will be returned.
 | ||||||
|  |     fn get_response(&mut self) -> Result<Option<Vec<u8>>> { | ||||||
|         self.get_until(b'>', true) |         self.get_until(b'>', true) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -85,6 +90,20 @@ impl Elm327 { | ||||||
|         Ok(device) |         Ok(device) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /// Flush the device's buffer
 | ||||||
|  |     pub fn flush(&mut self) -> Result<()> { | ||||||
|  |         thread::sleep(time::Duration::from_millis(500)); | ||||||
|  |         self.read_into_queue()?; | ||||||
|  |         self.buffer.clear(); | ||||||
|  |         thread::sleep(time::Duration::from_millis(500)); | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn flush_buffers(&mut self) -> Result<()> { | ||||||
|  |         self.device.usb_purge_buffers()?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     fn connect(&mut self, check_baud_rate: bool) -> Result<()> { |     fn connect(&mut self, check_baud_rate: bool) -> Result<()> { | ||||||
|         self.flush_buffers()?; |         self.flush_buffers()?; | ||||||
|         thread::sleep(time::Duration::from_millis(500)); |         thread::sleep(time::Duration::from_millis(500)); | ||||||
|  | @ -108,7 +127,7 @@ impl Elm327 { | ||||||
|         self.send_serial_cmd("ATZ")?; |         self.send_serial_cmd("ATZ")?; | ||||||
|         debug!( |         debug!( | ||||||
|             "reset_ic: got response {:?}", |             "reset_ic: got response {:?}", | ||||||
|             self.get_until_prompt()? |             self.get_response()? | ||||||
|                 .as_ref() |                 .as_ref() | ||||||
|                 .map(|l| std::str::from_utf8(l.as_slice())) |                 .map(|l| std::str::from_utf8(l.as_slice())) | ||||||
|         ); |         ); | ||||||
|  | @ -117,8 +136,14 @@ impl Elm327 { | ||||||
| 
 | 
 | ||||||
|     fn reset_protocol(&mut self) -> Result<()> { |     fn reset_protocol(&mut self) -> Result<()> { | ||||||
|         info!("Performing protocol reset"); |         info!("Performing protocol reset"); | ||||||
|         debug!("reset_protocol: got response {:?}", self.cmd("ATSP0")?); |         debug!( | ||||||
|         debug!("reset_protocol: got OBD response {:?}", self.cmd("0100")?); |             "reset_protocol: got response {:?}", | ||||||
|  |             self.serial_cmd("ATSP0")? | ||||||
|  |         ); | ||||||
|  |         debug!( | ||||||
|  |             "reset_protocol: got OBD response {:?}", | ||||||
|  |             self.cmd(&[0x01, 0x00])? | ||||||
|  |         ); | ||||||
|         self.flush_buffers()?; |         self.flush_buffers()?; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  | @ -148,7 +173,7 @@ impl Elm327 { | ||||||
|                         // our TX is bad
 |                         // our TX is bad
 | ||||||
|                         self.device.set_baud_rate(self.baud_rate)?; |                         self.device.set_baud_rate(self.baud_rate)?; | ||||||
|                         debug!("Baud rate bad - device did not receive response"); |                         debug!("Baud rate bad - device did not receive response"); | ||||||
|                         self.get_until_prompt()?; |                         self.get_response()?; | ||||||
|                     } |                     } | ||||||
|                 } else { |                 } else { | ||||||
|                     // reset baud rate and keep looking
 |                     // reset baud rate and keep looking
 | ||||||
|  | @ -160,11 +185,11 @@ impl Elm327 { | ||||||
|                             .as_ref() |                             .as_ref() | ||||||
|                             .map(|r| String::from_utf8_lossy(r)) |                             .map(|r| String::from_utf8_lossy(r)) | ||||||
|                     ); |                     ); | ||||||
|                     self.get_until_prompt()?; |                     self.get_response()?; | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 debug!("Baud rate bad - did not ok initially"); |                 debug!("Baud rate bad - did not ok initially"); | ||||||
|                 self.get_until_prompt()?; |                 self.get_response()?; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             thread::sleep(time::Duration::from_millis(200)); |             thread::sleep(time::Duration::from_millis(200)); | ||||||
|  | @ -222,25 +247,16 @@ impl Elm327 { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn get_byte(&mut self) -> Result<Option<u8>> { |     fn get_byte(&mut self) -> Result<Option<u8>> { | ||||||
|  |         match self.buffer.pop_front() { | ||||||
|  |             Some(b'\0') => Ok(None), | ||||||
|  |             Some(b) => Ok(Some(b)), | ||||||
|  |             None => { | ||||||
|                 self.read_into_queue()?; |                 self.read_into_queue()?; | ||||||
|         loop { |                 Ok(None) | ||||||
|             let b = self.buffer.pop_front(); |  | ||||||
|             if b != Some(b'\0') { |  | ||||||
|                 return Ok(b); |  | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn flush_buffers(&mut self) -> Result<()> { |  | ||||||
|         self.device.usb_purge_buffers()?; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn send_serial_str(&mut self, data: &str) -> Result<()> { |  | ||||||
|         self.device.write_all(data.as_bytes())?; |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     fn read_into_queue(&mut self) -> Result<()> { |     fn read_into_queue(&mut self) -> Result<()> { | ||||||
|         let mut buf = [0u8; 16]; |         let mut buf = [0u8; 16]; | ||||||
|         loop { |         loop { | ||||||
|  | @ -258,4 +274,19 @@ impl Elm327 { | ||||||
|         } |         } | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     fn send_serial_cmd(&mut self, data: &str) -> Result<()> { | ||||||
|  |         self.send_cmd(data.as_bytes()) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn serial_cmd(&mut self, cmd: &str) -> Result<Option<String>> { | ||||||
|  |         self.send_serial_cmd(cmd)?; | ||||||
|  |         self.get_response() | ||||||
|  |             .map(|o| o.and_then(|resp| String::from_utf8(resp).ok())) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fn send_serial_str(&mut self, data: &str) -> Result<()> { | ||||||
|  |         self.device.write_all(data.as_bytes())?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -3,22 +3,49 @@ pub use elm327::Elm327; | ||||||
| 
 | 
 | ||||||
| type Result<T> = std::result::Result<T, Error>; | type Result<T> = std::result::Result<T, Error>; | ||||||
| 
 | 
 | ||||||
|  | /// A lower-level API for using an OBD-II device
 | ||||||
| pub trait Obd2BaseDevice: Obd2Reader { | pub trait Obd2BaseDevice: Obd2Reader { | ||||||
|  |     /// Reset the device and the OBD-II interface
 | ||||||
|  |     ///
 | ||||||
|  |     /// First the device is reset, if it is stateful. Then the OBD-II interface is reinitialized,
 | ||||||
|  |     /// which resets the selected protocol on the device and rechecks the vehicle manufacturer if
 | ||||||
|  |     /// needed.
 | ||||||
|     fn reset(&mut self) -> Result<()>; |     fn reset(&mut self) -> Result<()>; | ||||||
|     fn flush(&mut self) -> Result<()>; | 
 | ||||||
|     fn send_serial_cmd(&mut self, data: &str) -> Result<()>; |     /// Send an OBD-II command
 | ||||||
|     fn cmd(&mut self, cmd: &str) -> Result<Option<String>> { |     fn send_cmd(&mut self, data: &[u8]) -> Result<()>; | ||||||
|         self.send_serial_cmd(cmd)?; | 
 | ||||||
|         self.get_until_prompt() |     /// Send an OBD-II command and get the reply
 | ||||||
|  |     ///
 | ||||||
|  |     /// The reply is decoded into a String of mostly hex data. Depending on the format of the
 | ||||||
|  |     /// response, some other characters may be included like line numbers for multiline responses
 | ||||||
|  |     /// (of the format "0: AB CD ...").
 | ||||||
|  |     fn cmd(&mut self, cmd: &[u8]) -> Result<Option<String>> { | ||||||
|  |         self.send_cmd(cmd)?; | ||||||
|  |         self.get_response() | ||||||
|             .map(|o| o.and_then(|resp| String::from_utf8(resp).ok())) |             .map(|o| o.and_then(|resp| String::from_utf8(resp).ok())) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// An API for reading OBD-II response data
 | ||||||
| pub trait Obd2Reader { | pub trait Obd2Reader { | ||||||
|  |     /// Try to get a single line of data from the device
 | ||||||
|  |     ///
 | ||||||
|  |     /// The trailing newline is not included. This function will never return an empty line, it
 | ||||||
|  |     /// will retry until a line with data is found. If no data is available after a reasonable
 | ||||||
|  |     /// timeout, `Ok(None)` will be returned.
 | ||||||
|     fn get_line(&mut self) -> Result<Option<Vec<u8>>>; |     fn get_line(&mut self) -> Result<Option<Vec<u8>>>; | ||||||
|     fn get_until_prompt(&mut self) -> Result<Option<Vec<u8>>>; | 
 | ||||||
|  |     /// Get an entire OBD-II response
 | ||||||
|  |     ///
 | ||||||
|  |     /// Empty vectors are allowed to be returned. This function should always be called after a
 | ||||||
|  |     /// command is sent, possibly after calling [get_line](Self::get_line) to read the first lines,
 | ||||||
|  |     /// so that any metadata sent by the device after the response from the vehicle can be dealt
 | ||||||
|  |     /// with.
 | ||||||
|  |     fn get_response(&mut self) -> Result<Option<Vec<u8>>>; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Error type for low-level ODB-II communication issues
 | ||||||
| #[derive(thiserror::Error, Debug)] | #[derive(thiserror::Error, Debug)] | ||||||
| pub enum Error { | pub enum Error { | ||||||
|     #[error("FTDI error: `{0:?}`")] |     #[error("FTDI error: `{0:?}`")] | ||||||
|  |  | ||||||
|  | @ -9,7 +9,7 @@ pub struct Obd2<T: Obd2BaseDevice> { | ||||||
| 
 | 
 | ||||||
| impl<T: Obd2BaseDevice> Obd2Device for Obd2<T> { | impl<T: Obd2BaseDevice> Obd2Device for Obd2<T> { | ||||||
|     fn obd_command(&mut self, mode: u8, pid: u8) -> Result<Vec<Vec<u8>>> { |     fn obd_command(&mut self, mode: u8, pid: u8) -> Result<Vec<Vec<u8>>> { | ||||||
|         let result = self.command(&format!("{:02x}{:02x}", mode, pid))?; |         let result = self.command(&[mode, pid])?; | ||||||
| 
 | 
 | ||||||
|         for response in result.iter() { |         for response in result.iter() { | ||||||
|             if response.first() != Some(&(0x40 | mode)) { |             if response.first() != Some(&(0x40 | mode)) { | ||||||
|  | @ -24,7 +24,7 @@ impl<T: Obd2BaseDevice> Obd2Device for Obd2<T> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn obd_mode_command(&mut self, mode: u8) -> Result<Vec<Vec<u8>>> { |     fn obd_mode_command(&mut self, mode: u8) -> Result<Vec<Vec<u8>>> { | ||||||
|         let result = self.command(&format!("{:02x}", mode))?; |         let result = self.command(std::slice::from_ref(&mode))?; | ||||||
| 
 | 
 | ||||||
|         for response in result.iter() { |         for response in result.iter() { | ||||||
|             if response.first() != Some(&(0x40 | mode)) { |             if response.first() != Some(&(0x40 | mode)) { | ||||||
|  | @ -37,7 +37,7 @@ impl<T: Obd2BaseDevice> Obd2Device for Obd2<T> { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl<T: Obd2BaseDevice> Obd2<T> { | impl<T: Obd2BaseDevice> Obd2<T> { | ||||||
|     fn command(&mut self, command: &str) -> Result<Vec<Vec<u8>>> { |     fn command(&mut self, command: &[u8]) -> Result<Vec<Vec<u8>>> { | ||||||
|         let response = self |         let response = self | ||||||
|             .device |             .device | ||||||
|             .cmd(command)? |             .cmd(command)? | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Robert Sammelson
						Robert Sammelson