Skip to main content

obd2_core/session/
diag_session.rs

1//! Diagnostic session management — Mode 10/27/2F/3E.
2//!
3//! Implements the session control sequence required for actuator tests:
4//! 1. enter_diagnostic_session(Extended)  — Mode 10
5//! 2. security_access(module, key_fn)     — Mode 27
6//! 3. actuator_control(did, module, cmd)  — Mode 2F
7//!
8//! Tester Present (Mode 3E) keep-alive runs automatically during
9//! extended sessions.
10
11use crate::adapter::Adapter;
12use crate::error::Obd2Error;
13use crate::protocol::service::{ServiceRequest, Target, DiagSession, ActuatorCommand};
14use tokio::sync::watch;
15
16/// State of the diagnostic session.
17#[derive(Debug, Clone, PartialEq, Eq, Default)]
18pub enum SessionState {
19    /// Default session — no special access.
20    #[default]
21    Default,
22    /// Extended session active — Mode 2F available after security access.
23    Extended {
24        /// Modules that have been security-unlocked.
25        unlocked_modules: Vec<String>,
26    },
27    /// Programming session (for reflash — library provides no flash capability).
28    Programming,
29}
30
31
32/// Callback type for computing the security key from a seed.
33/// The algorithm is manufacturer-proprietary (BR-7.2).
34pub type KeyFunction = Box<dyn Fn(&[u8]) -> Vec<u8> + Send>;
35
36/// Enter a diagnostic session (Mode 10).
37///
38/// Starts tester-present keep-alive automatically (BR-3.6).
39pub async fn enter_session<A: Adapter>(
40    adapter: &mut A,
41    session: DiagSession,
42    module: &str,
43) -> Result<SessionState, Obd2Error> {
44    let sub = match session {
45        DiagSession::Default => 0x01,
46        DiagSession::Programming => 0x02,
47        DiagSession::Extended => 0x03,
48    };
49
50    let req = ServiceRequest {
51        service_id: 0x10,
52        data: vec![sub],
53        target: Target::Module(module.to_string()),
54    };
55
56    adapter.request(&req).await?;
57
58    let state = match session {
59        DiagSession::Default => SessionState::Default,
60        DiagSession::Extended => SessionState::Extended { unlocked_modules: vec![] },
61        DiagSession::Programming => SessionState::Programming,
62    };
63
64    tracing::info!(session = ?session, module = module, "entered diagnostic session");
65    Ok(state)
66}
67
68/// Request security access (Mode 27) — seed/key exchange.
69///
70/// Step 1: Request seed (sub-function 0x01)
71/// Step 2: Send key (sub-function 0x02) computed by the caller's KeyFunction
72pub async fn security_access<A: Adapter>(
73    adapter: &mut A,
74    module: &str,
75    key_fn: &KeyFunction,
76) -> Result<(), Obd2Error> {
77    // Step 1: Request seed
78    let seed_req = ServiceRequest {
79        service_id: 0x27,
80        data: vec![0x01],
81        target: Target::Module(module.to_string()),
82    };
83    let seed = adapter.request(&seed_req).await?;
84
85    if seed.is_empty() {
86        return Err(Obd2Error::Adapter("empty seed from Mode 27".into()));
87    }
88
89    // Check if already unlocked (seed = all zeros)
90    if seed.iter().all(|&b| b == 0) {
91        tracing::info!(module = module, "security already unlocked (zero seed)");
92        return Ok(());
93    }
94
95    // Step 2: Compute key and send
96    let key = key_fn(&seed);
97
98    let key_req = ServiceRequest {
99        service_id: 0x27,
100        data: std::iter::once(0x02).chain(key.into_iter()).collect(),
101        target: Target::Module(module.to_string()),
102    };
103    adapter.request(&key_req).await?;
104
105    tracing::info!(module = module, "security access granted");
106    Ok(())
107}
108
109/// Send an actuator control command (Mode 2F).
110///
111/// Requires active extended session + security access (BR-7.1).
112pub async fn actuator_control<A: Adapter>(
113    adapter: &mut A,
114    did: u16,
115    module: &str,
116    command: &ActuatorCommand,
117    state: &SessionState,
118) -> Result<(), Obd2Error> {
119    // Verify session state (BR-7.1)
120    match state {
121        SessionState::Extended { unlocked_modules } => {
122            if !unlocked_modules.contains(&module.to_string()) {
123                return Err(Obd2Error::SecurityRequired);
124            }
125        }
126        _ => return Err(Obd2Error::SecurityRequired),
127    }
128
129    let did_bytes = [(did >> 8) as u8, (did & 0xFF) as u8];
130    let control_bytes = match command {
131        ActuatorCommand::ReturnToEcu => vec![0x00],
132        ActuatorCommand::Activate => vec![0x03],
133        ActuatorCommand::Adjust(data) => {
134            let mut v = vec![0x03];
135            v.extend(data);
136            v
137        }
138    };
139
140    let mut data = Vec::new();
141    data.extend_from_slice(&did_bytes);
142    data.extend(control_bytes);
143
144    let req = ServiceRequest {
145        service_id: 0x2F,
146        data,
147        target: Target::Module(module.to_string()),
148    };
149
150    tracing::warn!(did = format!("{:#06X}", did), module = module, "actuator control command");
151    adapter.request(&req).await?;
152    Ok(())
153}
154
155/// Release actuator control — return to ECU (Mode 2F with ReturnToEcu).
156pub async fn actuator_release<A: Adapter>(
157    adapter: &mut A,
158    did: u16,
159    module: &str,
160) -> Result<(), Obd2Error> {
161    let did_bytes = [(did >> 8) as u8, (did & 0xFF) as u8];
162    let req = ServiceRequest {
163        service_id: 0x2F,
164        data: vec![did_bytes[0], did_bytes[1], 0x00],
165        target: Target::Module(module.to_string()),
166    };
167    adapter.request(&req).await?;
168    tracing::info!(did = format!("{:#06X}", did), module = module, "actuator released to ECU");
169    Ok(())
170}
171
172/// End diagnostic session — return to default (Mode 10 sub 0x01).
173pub async fn end_session<A: Adapter>(
174    adapter: &mut A,
175    module: &str,
176) -> Result<(), Obd2Error> {
177    let req = ServiceRequest {
178        service_id: 0x10,
179        data: vec![0x01], // Default session
180        target: Target::Module(module.to_string()),
181    };
182    adapter.request(&req).await?;
183    tracing::info!(module = module, "returned to default diagnostic session");
184    Ok(())
185}
186
187/// Send Tester Present (Mode 3E) to keep a diagnostic session alive.
188pub async fn tester_present<A: Adapter>(
189    adapter: &mut A,
190    module: &str,
191) -> Result<(), Obd2Error> {
192    let req = ServiceRequest {
193        service_id: 0x3E,
194        data: vec![],
195        target: Target::Module(module.to_string()),
196    };
197    adapter.request(&req).await?;
198    Ok(())
199}
200
201/// Spawn a background task that sends Tester Present every 2 seconds.
202/// Returns a cancel sender — drop it or send to stop the keep-alive.
203pub fn start_tester_present_keepalive() -> watch::Sender<bool> {
204    let (cancel_tx, _cancel_rx) = watch::channel(false);
205    // Note: The actual keep-alive task needs adapter access,
206    // which Session will manage. This just provides the cancel mechanism.
207    cancel_tx
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::adapter::mock::MockAdapter;
214
215    #[tokio::test]
216    async fn test_enter_extended_session() {
217        let mut adapter = MockAdapter::new();
218        adapter.initialize().await.unwrap();
219        let state = enter_session(&mut adapter, DiagSession::Extended, "ecm").await.unwrap();
220        assert!(matches!(state, SessionState::Extended { .. }));
221    }
222
223    #[tokio::test]
224    async fn test_enter_default_session() {
225        let mut adapter = MockAdapter::new();
226        adapter.initialize().await.unwrap();
227        let state = enter_session(&mut adapter, DiagSession::Default, "ecm").await.unwrap();
228        assert_eq!(state, SessionState::Default);
229    }
230
231    #[tokio::test]
232    async fn test_end_session() {
233        let mut adapter = MockAdapter::new();
234        adapter.initialize().await.unwrap();
235        let result = end_session(&mut adapter, "ecm").await;
236        assert!(result.is_ok());
237    }
238
239    #[tokio::test]
240    async fn test_actuator_requires_security() {
241        let mut adapter = MockAdapter::new();
242        let state = SessionState::Default; // not extended
243        let result = actuator_control(
244            &mut adapter, 0x1196, "ecm",
245            &ActuatorCommand::Activate, &state,
246        ).await;
247        assert!(matches!(result, Err(Obd2Error::SecurityRequired)));
248    }
249
250    #[tokio::test]
251    async fn test_actuator_requires_unlock() {
252        let mut adapter = MockAdapter::new();
253        let state = SessionState::Extended {
254            unlocked_modules: vec![], // ecm not unlocked
255        };
256        let result = actuator_control(
257            &mut adapter, 0x1196, "ecm",
258            &ActuatorCommand::Activate, &state,
259        ).await;
260        assert!(matches!(result, Err(Obd2Error::SecurityRequired)));
261    }
262
263    #[tokio::test]
264    async fn test_actuator_with_security() {
265        let mut adapter = MockAdapter::new();
266        adapter.initialize().await.unwrap();
267        let state = SessionState::Extended {
268            unlocked_modules: vec!["ecm".to_string()],
269        };
270        let result = actuator_control(
271            &mut adapter, 0x1196, "ecm",
272            &ActuatorCommand::Activate, &state,
273        ).await;
274        assert!(result.is_ok());
275    }
276
277    #[tokio::test]
278    async fn test_tester_present() {
279        let mut adapter = MockAdapter::new();
280        adapter.initialize().await.unwrap();
281        let result = tester_present(&mut adapter, "ecm").await;
282        assert!(result.is_ok());
283    }
284
285    #[test]
286    fn test_session_state_default() {
287        let state = SessionState::default();
288        assert_eq!(state, SessionState::Default);
289    }
290}