Skip to main content

obd2_core/
error.rs

1//! Error types for obd2-core.
2
3/// OBD-II / UDS Negative Response Code.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5#[non_exhaustive]
6pub enum NegativeResponse {
7    /// 0x10
8    GeneralReject,
9    /// 0x11
10    ServiceNotSupported,
11    /// 0x12
12    SubFunctionNotSupported,
13    /// 0x13
14    IncorrectMessageLength,
15    /// 0x14
16    ResponseTooLong,
17    /// 0x22
18    ConditionsNotCorrect,
19    /// 0x31
20    RequestOutOfRange,
21    /// 0x33
22    SecurityAccessDenied,
23    /// 0x35
24    InvalidKey,
25    /// 0x36
26    ExceededAttempts,
27    /// 0x37
28    TimeDelayNotExpired,
29    /// 0x72
30    GeneralProgrammingFailure,
31    /// 0x78
32    ResponsePending,
33}
34
35impl NegativeResponse {
36    pub fn from_byte(b: u8) -> Option<Self> {
37        match b {
38            0x10 => Some(Self::GeneralReject),
39            0x11 => Some(Self::ServiceNotSupported),
40            0x12 => Some(Self::SubFunctionNotSupported),
41            0x13 => Some(Self::IncorrectMessageLength),
42            0x14 => Some(Self::ResponseTooLong),
43            0x22 => Some(Self::ConditionsNotCorrect),
44            0x31 => Some(Self::RequestOutOfRange),
45            0x33 => Some(Self::SecurityAccessDenied),
46            0x35 => Some(Self::InvalidKey),
47            0x36 => Some(Self::ExceededAttempts),
48            0x37 => Some(Self::TimeDelayNotExpired),
49            0x72 => Some(Self::GeneralProgrammingFailure),
50            0x78 => Some(Self::ResponsePending),
51            _ => None,
52        }
53    }
54
55    pub fn code(&self) -> u8 {
56        match self {
57            Self::GeneralReject => 0x10,
58            Self::ServiceNotSupported => 0x11,
59            Self::SubFunctionNotSupported => 0x12,
60            Self::IncorrectMessageLength => 0x13,
61            Self::ResponseTooLong => 0x14,
62            Self::ConditionsNotCorrect => 0x22,
63            Self::RequestOutOfRange => 0x31,
64            Self::SecurityAccessDenied => 0x33,
65            Self::InvalidKey => 0x35,
66            Self::ExceededAttempts => 0x36,
67            Self::TimeDelayNotExpired => 0x37,
68            Self::GeneralProgrammingFailure => 0x72,
69            Self::ResponsePending => 0x78,
70        }
71    }
72}
73
74impl std::fmt::Display for NegativeResponse {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        write!(f, "{:?} (0x{:02X})", self, self.code())
77    }
78}
79
80/// Errors returned by obd2-core operations.
81#[derive(Debug, thiserror::Error)]
82#[non_exhaustive]
83pub enum Obd2Error {
84    #[error("transport error: {0}")]
85    Transport(String),
86
87    #[error("adapter error: {0}")]
88    Adapter(String),
89
90    #[error("adapter is busy (stop polling first)")]
91    AdapterBusy,
92
93    #[error("timeout waiting for response")]
94    Timeout,
95
96    #[error("no data (vehicle did not respond)")]
97    NoData,
98
99    #[error("PID {pid:#04x} not supported by this vehicle")]
100    UnsupportedPid { pid: u8 },
101
102    #[error("module '{0}' not found in vehicle spec")]
103    ModuleNotFound(String),
104
105    #[error("negative response: {nrc} for service {service:#04x}")]
106    NegativeResponse { service: u8, nrc: NegativeResponse },
107
108    #[error("security access required (call enter_diagnostic_session + security_access first)")]
109    SecurityRequired,
110
111    #[error("no vehicle spec matched — load a spec or call identify_vehicle()")]
112    NoSpec,
113
114    #[error("bus '{0}' not available on this vehicle")]
115    BusNotAvailable(String),
116
117    #[error("spec parse error: {0}")]
118    SpecParse(String),
119
120    #[error("parse error: {0}")]
121    ParseError(String),
122
123    #[error("IO error: {0}")]
124    Io(#[from] std::io::Error),
125
126    #[error(transparent)]
127    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_error_display_timeout() {
136        let err = Obd2Error::Timeout;
137        assert_eq!(err.to_string(), "timeout waiting for response");
138    }
139
140    #[test]
141    fn test_error_display_nrc() {
142        let err = Obd2Error::NegativeResponse {
143            service: 0x22,
144            nrc: NegativeResponse::RequestOutOfRange,
145        };
146        let s = err.to_string();
147        assert!(s.contains("RequestOutOfRange"));
148        assert!(s.contains("0x22"));
149    }
150
151    #[test]
152    fn test_io_error_conversion() {
153        let io_err = std::io::Error::new(std::io::ErrorKind::BrokenPipe, "gone");
154        let obd_err: Obd2Error = io_err.into();
155        assert!(matches!(obd_err, Obd2Error::Io(_)));
156    }
157
158    #[test]
159    fn test_nrc_from_byte() {
160        assert_eq!(
161            NegativeResponse::from_byte(0x31),
162            Some(NegativeResponse::RequestOutOfRange)
163        );
164        assert_eq!(
165            NegativeResponse::from_byte(0x78),
166            Some(NegativeResponse::ResponsePending)
167        );
168        assert_eq!(NegativeResponse::from_byte(0xFF), None);
169    }
170
171    #[test]
172    fn test_nrc_code() {
173        assert_eq!(NegativeResponse::GeneralReject.code(), 0x10);
174        assert_eq!(NegativeResponse::ResponsePending.code(), 0x78);
175    }
176
177    #[test]
178    fn test_nrc_display() {
179        let nrc = NegativeResponse::SecurityAccessDenied;
180        let s = format!("{}", nrc);
181        assert!(s.contains("SecurityAccessDenied"));
182        assert!(s.contains("0x33"));
183    }
184
185    #[test]
186    fn test_error_is_send_sync() {
187        fn assert_send<T: Send>() {}
188        fn assert_sync<T: Sync>() {}
189        // Obd2Error must be Send (required for async)
190        assert_send::<Obd2Error>();
191    }
192}