Skip to main content

obd2_core/adapter/
elm327.rs

1//! ELM327/STN adapter implementation.
2//!
3//! Translates OBD-II service requests into ELM327 AT commands
4//! and hex string format, parses responses back to raw bytes.
5
6use std::collections::HashSet;
7use async_trait::async_trait;
8use tracing::debug;
9use crate::error::Obd2Error;
10use crate::protocol::pid::Pid;
11use crate::protocol::service::{ServiceRequest, Target};
12use crate::transport::Transport;
13use super::{Adapter, AdapterInfo, Chipset, Capabilities};
14use crate::vehicle::Protocol;
15
16/// Default adapter info returned before initialization.
17fn default_adapter_info() -> AdapterInfo {
18    AdapterInfo {
19        chipset: Chipset::Unknown,
20        firmware: String::new(),
21        protocol: Protocol::Auto,
22        capabilities: Capabilities::default(),
23    }
24}
25
26/// ELM327/STN adapter over any Transport.
27pub struct Elm327Adapter {
28    transport: Box<dyn Transport>,
29    info: AdapterInfo,
30    initialized: bool,
31    current_header: Option<String>,
32}
33
34impl Elm327Adapter {
35    /// Create a new ELM327 adapter wrapping a transport.
36    pub fn new(transport: Box<dyn Transport>) -> Self {
37        Self {
38            transport,
39            info: default_adapter_info(),
40            initialized: false,
41            current_header: None,
42        }
43    }
44
45    /// Send an AT command and read the response.
46    async fn send_command(&mut self, cmd: &str) -> Result<String, Obd2Error> {
47        debug!(cmd = cmd, "ELM327 send");
48        self.transport.write(format!("{}\r", cmd).as_bytes()).await?;
49        let response_bytes = self.transport.read().await?;
50        let response = String::from_utf8_lossy(&response_bytes).to_string();
51        debug!(response = response.trim(), "ELM327 recv");
52        Ok(response)
53    }
54
55    /// Set the J1850/CAN header for addressing a specific module.
56    #[allow(dead_code)]
57    async fn set_header(&mut self, header: &str) -> Result<(), Obd2Error> {
58        if self.current_header.as_deref() == Some(header) {
59            return Ok(()); // Already set
60        }
61        let response = self.send_command(&format!("AT SH {}", header)).await?;
62        if !response.contains("OK") {
63            return Err(Obd2Error::Adapter(format!("AT SH failed: {}", response.trim())));
64        }
65        self.current_header = Some(header.to_string());
66        Ok(())
67    }
68
69    /// Parse hex response string into raw bytes.
70    /// Input like "41 0C 0A A0\r>" -> vec of all hex bytes, then caller
71    /// specifies how many leading bytes to skip (service echo + PID echo).
72    fn parse_hex_response(response: &str, skip_bytes: usize) -> Result<Vec<u8>, Obd2Error> {
73        let cleaned = response
74            .replace(['\r', '\n'], " ")
75            .replace('>', "");
76
77        let bytes: Result<Vec<u8>, _> = cleaned
78            .split_whitespace()
79            .filter(|s| !s.is_empty())
80            .map(|s| u8::from_str_radix(s, 16))
81            .collect();
82
83        match bytes {
84            Ok(all_bytes) => {
85                if all_bytes.len() > skip_bytes {
86                    Ok(all_bytes[skip_bytes..].to_vec())
87                } else {
88                    Ok(vec![])
89                }
90            }
91            Err(_) => Err(Obd2Error::ParseError(format!(
92                "invalid hex: {}",
93                response.trim()
94            ))),
95        }
96    }
97
98    /// Parse supported PIDs from a bitmap response.
99    /// `data` is the 4 bitmap bytes, `base_pid` is the PID that was queried
100    /// (0x00, 0x20, 0x40, 0x60). Bit 31 (MSB of first byte) = base_pid + 1.
101    pub fn parse_supported_pids(data: &[u8], base_pid: u8) -> Vec<u8> {
102        let mut pids = Vec::new();
103        for (byte_idx, &byte) in data.iter().enumerate() {
104            for bit in 0..8 {
105                if byte & (0x80 >> bit) != 0 {
106                    let pid = base_pid + (byte_idx as u8 * 8) + bit + 1;
107                    pids.push(pid);
108                }
109            }
110        }
111        pids
112    }
113
114    /// Check if response indicates an error.
115    fn check_response_error(response: &str) -> Result<(), Obd2Error> {
116        let trimmed = response.trim();
117        if trimmed.contains("NO DATA") {
118            return Err(Obd2Error::NoData);
119        }
120        if trimmed.contains("UNABLE TO CONNECT") {
121            return Err(Obd2Error::Adapter("unable to connect to vehicle".into()));
122        }
123        if trimmed.contains("BUS INIT") && trimmed.contains("ERROR") {
124            return Err(Obd2Error::Adapter("bus initialization error".into()));
125        }
126        if trimmed.contains("CAN ERROR") {
127            return Err(Obd2Error::Adapter("CAN bus error".into()));
128        }
129        if trimmed == "?" {
130            return Err(Obd2Error::Adapter("unknown command".into()));
131        }
132        // Check for negative response (7F xx xx)
133        if trimmed.starts_with("7F") {
134            let parts: Vec<&str> = trimmed.split_whitespace().collect();
135            if parts.len() >= 3 {
136                if let (Ok(service), Ok(nrc_byte)) = (
137                    u8::from_str_radix(parts[1], 16),
138                    u8::from_str_radix(parts[2], 16),
139                ) {
140                    if let Some(nrc) = crate::error::NegativeResponse::from_byte(nrc_byte) {
141                        return Err(Obd2Error::NegativeResponse { service, nrc });
142                    }
143                }
144            }
145        }
146        Ok(())
147    }
148}
149
150impl std::fmt::Debug for Elm327Adapter {
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        f.debug_struct("Elm327Adapter")
153            .field("info", &self.info)
154            .field("initialized", &self.initialized)
155            .field("current_header", &self.current_header)
156            .finish()
157    }
158}
159
160#[async_trait]
161impl Adapter for Elm327Adapter {
162    async fn initialize(&mut self) -> Result<AdapterInfo, Obd2Error> {
163        // Step 1: Reset
164        let atz_response = self.send_command("ATZ").await?;
165
166        // Step 2: Probe for STN
167        let sti_response = match self.send_command("STI").await {
168            Ok(r) if !r.contains("?") => Some(r),
169            _ => None,
170        };
171
172        // Detect chipset and capabilities
173        let mut info = AdapterInfo::detect(
174            &atz_response,
175            sti_response.as_deref(),
176        );
177
178        // Step 3-6: Configure
179        self.send_command("ATE0").await?;   // Echo off
180        self.send_command("ATL0").await?;   // Linefeeds off
181        self.send_command("ATH0").await?;   // Headers off (for standard queries)
182        self.send_command("ATSP0").await?;  // Auto-detect protocol
183
184        // Step 7: Query supported PIDs to detect protocol
185        let response = self.send_command("0100").await?;
186        if response.contains("41 00") || response.contains("4100") {
187            // Protocol detected successfully
188            // Try to detect which protocol was auto-selected
189            if let Ok(protocol_response) = self.send_command("ATDPN").await {
190                let proto_char = protocol_response
191                    .trim()
192                    .replace('>', "")
193                    .trim()
194                    .chars()
195                    .last()
196                    .unwrap_or('0');
197                info.protocol = match proto_char {
198                    '1' => Protocol::J1850Pwm,
199                    '2' => Protocol::J1850Vpw,
200                    '3' => Protocol::Iso9141(crate::vehicle::KLineInit::SlowInit),
201                    '4' => Protocol::Kwp2000(crate::vehicle::KLineInit::SlowInit),
202                    '5' => Protocol::Kwp2000(crate::vehicle::KLineInit::FastInit),
203                    '6' => Protocol::Can11Bit500,
204                    '7' => Protocol::Can29Bit500,
205                    '8' => Protocol::Can11Bit250,
206                    '9' => Protocol::Can29Bit250,
207                    _ => Protocol::Auto,
208                };
209            }
210        }
211
212        self.info = info.clone();
213        self.initialized = true;
214        Ok(info)
215    }
216
217    async fn request(&mut self, req: &ServiceRequest) -> Result<Vec<u8>, Obd2Error> {
218        // Handle targeting
219        match &req.target {
220            Target::Module(module_id) => {
221                debug!(module = %module_id, "targeting specific module");
222            }
223            Target::Broadcast => {
224                // Use default functional addressing
225            }
226        }
227
228        // Build the hex command string
229        let cmd = if req.data.is_empty() {
230            format!("{:02X}", req.service_id)
231        } else {
232            let data_hex: Vec<String> = req.data.iter().map(|b| format!("{:02X}", b)).collect();
233            format!("{:02X}{}", req.service_id, data_hex.join(""))
234        };
235
236        let response = self.send_command(&cmd).await?;
237
238        // Check for errors
239        Self::check_response_error(&response)?;
240
241        // Determine how many echo bytes to skip
242        let skip = match req.service_id {
243            0x01 | 0x02 => 2,  // service echo + PID echo
244            0x03 | 0x04 | 0x07 | 0x0A => 1,  // service echo only
245            0x09 => 2,  // service echo + infotype
246            0x22 | 0x21 => 3,  // service echo + 2-byte DID echo
247            _ => 1,
248        };
249
250        Self::parse_hex_response(&response, skip)
251    }
252
253    async fn supported_pids(&mut self) -> Result<HashSet<Pid>, Obd2Error> {
254        let mut all_supported = HashSet::new();
255
256        // Query PID 0x00, 0x20, 0x40, 0x60
257        for base in [0x00u8, 0x20, 0x40, 0x60] {
258            let cmd = format!("01{:02X}", base);
259            match self.send_command(&cmd).await {
260                Ok(response) => {
261                    if Self::check_response_error(&response).is_err() {
262                        break; // No more supported PIDs
263                    }
264                    if let Ok(data) = Self::parse_hex_response(&response, 2) {
265                        if data.len() >= 4 {
266                            for pid_code in Self::parse_supported_pids(&data, base) {
267                                all_supported.insert(Pid(pid_code));
268                            }
269                        }
270                    }
271                }
272                Err(_) => break,
273            }
274        }
275
276        Ok(all_supported)
277    }
278
279    async fn battery_voltage(&mut self) -> Result<Option<f64>, Obd2Error> {
280        let response = self.send_command("ATRV").await?;
281        let cleaned = response
282            .replace(['V', 'v', '>', '\r', '\n'], "");
283        let cleaned = cleaned.trim().to_string();
284        match cleaned.parse::<f64>() {
285            Ok(v) => Ok(Some(v)),
286            Err(_) => Ok(None),
287        }
288    }
289
290    fn info(&self) -> &AdapterInfo {
291        &self.info
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use crate::transport::mock::MockTransport;
299
300    fn setup_init(transport: &mut MockTransport) {
301        transport.expect("ATZ", "ELM327 v2.1\r\r>");
302        transport.expect("STI", "?\r>");
303        transport.expect("ATE0", "OK\r>");
304        transport.expect("ATL0", "OK\r>");
305        transport.expect("ATH0", "OK\r>");
306        transport.expect("ATSP0", "OK\r>");
307        transport.expect("0100", "41 00 BE 3E B8 11\r>");
308        transport.expect("ATDPN", "A6\r>"); // CAN 11-bit 500kbps
309    }
310
311    #[tokio::test]
312    async fn test_elm327_initialize() {
313        let mut transport = MockTransport::new();
314        setup_init(&mut transport);
315
316        let mut adapter = Elm327Adapter::new(Box::new(transport));
317        let info = adapter.initialize().await.unwrap();
318        assert_eq!(info.chipset, Chipset::Elm327Genuine);
319    }
320
321    #[tokio::test]
322    async fn test_elm327_read_pid() {
323        let mut transport = MockTransport::new();
324        setup_init(&mut transport);
325        transport.expect("010C", "41 0C 0A A0\r>");
326
327        let mut adapter = Elm327Adapter::new(Box::new(transport));
328        adapter.initialize().await.unwrap();
329
330        let req = ServiceRequest::read_pid(Pid::ENGINE_RPM);
331        let response = adapter.request(&req).await.unwrap();
332        assert_eq!(response, vec![0x0A, 0xA0]);
333    }
334
335    #[tokio::test]
336    async fn test_elm327_no_data() {
337        let mut transport = MockTransport::new();
338        setup_init(&mut transport);
339        transport.expect("015C", "NO DATA\r>");
340
341        let mut adapter = Elm327Adapter::new(Box::new(transport));
342        adapter.initialize().await.unwrap();
343
344        let req = ServiceRequest::read_pid(Pid::ENGINE_OIL_TEMP);
345        let result = adapter.request(&req).await;
346        assert!(matches!(result, Err(Obd2Error::NoData)));
347    }
348
349    #[tokio::test]
350    async fn test_elm327_parse_hex_response() {
351        let data = Elm327Adapter::parse_hex_response("41 0C 0A A0\r>", 2).unwrap();
352        assert_eq!(data, vec![0x0A, 0xA0]);
353    }
354
355    #[tokio::test]
356    async fn test_elm327_parse_supported_pids() {
357        // BE 3E B8 11 = supported PIDs bitmap
358        let pids = Elm327Adapter::parse_supported_pids(&[0xBE, 0x3E, 0xB8, 0x11], 0x00);
359        assert!(pids.contains(&0x01)); // Monitor status
360        assert!(pids.contains(&0x04)); // Engine load
361        assert!(pids.contains(&0x05)); // Coolant temp
362        assert!(pids.contains(&0x0C)); // RPM
363        assert!(pids.contains(&0x0D)); // Speed
364    }
365
366    #[tokio::test]
367    async fn test_elm327_read_dtcs() {
368        let mut transport = MockTransport::new();
369        setup_init(&mut transport);
370        transport.expect("03", "43 01 04 20 01 71\r>");
371
372        let mut adapter = Elm327Adapter::new(Box::new(transport));
373        adapter.initialize().await.unwrap();
374
375        let req = ServiceRequest::read_dtcs();
376        let response = adapter.request(&req).await.unwrap();
377        // After skipping the service echo byte (43), we should have the DTC data
378        assert!(!response.is_empty());
379    }
380
381    #[tokio::test]
382    async fn test_elm327_battery_voltage() {
383        let mut transport = MockTransport::new();
384        setup_init(&mut transport);
385        transport.expect("ATRV", "14.4V\r>");
386
387        let mut adapter = Elm327Adapter::new(Box::new(transport));
388        adapter.initialize().await.unwrap();
389
390        let voltage = adapter.battery_voltage().await.unwrap();
391        assert_eq!(voltage, Some(14.4));
392    }
393
394    #[tokio::test]
395    async fn test_elm327_negative_response() {
396        let result = Elm327Adapter::check_response_error("7F 22 31\r>");
397        assert!(matches!(result, Err(Obd2Error::NegativeResponse { service: 0x22, .. })));
398    }
399}