Skip to main content

ace_doip/session/
connection.rs

1//! Per-TCP-connection state machine
2//!
3//! Each TCP connection to the gateway has its own `ConnectionState` which owns the activation line
4//! state machine and tracks alive check timing. The gateway creates one `ConnectionState` per
5//! accepted TCP connection and drops it when the connection closes.
6
7// region: Imports
8
9use crate::{
10    header::PayloadType,
11    payload::{DiagnosticNackCode, RoutingActivationRequest, RoutingActivationResponse},
12    session::{ActivationAuthProvider, ActivationDenialReason, ActivationStateMachine},
13};
14use ace_core::FrameRead;
15use ace_sim::clock::{Duration, Instant};
16
17// endregion: Imports
18
19// region: ConnectionConfig
20
21/// Per-connection timing configuration.
22#[derive(Debug, Clone)]
23pub struct ConnectionConfig {
24    /// How long to wait for an alive check response before considering the connection dead.
25    pub alive_check_timeout: Duration,
26
27    /// How long an active connection may be idle before the gateway sends an alive check request.
28    pub idle_timeout: Duration,
29}
30
31impl Default for ConnectionConfig {
32    fn default() -> Self {
33        Self {
34            alive_check_timeout: Duration::from_millis(500),
35            idle_timeout: Duration::from_millis(5_000),
36        }
37    }
38}
39
40// endregion: ConnectionConfig
41
42// region: ConnectionPhase
43
44/// The life-cycle phase of a TCP connection.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum ConnectionPhase {
47    /// TCP connected, waiting for RoutingActivationRequest.
48    Connected,
49
50    /// Routing activation complete - diagnostic messages permitted.
51    Active,
52
53    /// Gateway sent an AliveCheckRequest, waiting for response.
54    AliveCheckPending { sent_at: Instant },
55
56    /// Connection is being torn down.
57    Closing,
58}
59
60// endregion: ConnectionPhase
61
62// region: ConnectionEvent
63
64/// Events produced by the connection state machine.
65///
66/// The gateway collects these after each `handle` or `tick` call and acts on them - sending
67/// frames, routing messages, closing sockets.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ConnectionEvent<const BUF: usize = 4096> {
70    /// Send this `RoutingActivationResponse` frame back to the tester.
71    SendActivationResponse(RoutingActivationResponse),
72
73    /// Forward these raw UDS bytes to the ECU at the given target address.
74    ForwardToEcu {
75        source_address: u16,
76        target_address: u16,
77        uds_data: heapless::Vec<u8, BUF>,
78    },
79
80    /// Send a `DiagnosticMessageAck` back to the tester.
81    SendDiagnosticAck {
82        source_address: u16,
83        target_address: u16,
84    },
85
86    /// Send a `DiagnosticMessageNack` back to the tester.
87    SendDiagnosticNack {
88        source_address: u16,
89        target_address: u16,
90        nack_code: DiagnosticNackCode,
91    },
92
93    /// Send an `AliveCheckRequest` to the tester.
94    SendAliveCheckRequest,
95
96    /// Send an `AliveCheckResponse` back to the tester.
97    SendAliveCheckResponse,
98
99    /// Close this TCP connection - activation denied or alive check failed.
100    Close,
101}
102
103// endregion: ConnectionEvent
104
105// region: ConnectionState
106
107/// State machine for a single TCP connection to the gateway.
108///
109/// Owns the `ActivationStateMachine` for this connection and tracks idle/alive-check timing. The
110/// gateway drives this via `handle_frame` and `tick`.
111#[derive(Debug)]
112pub struct ConnectionState<A: ActivationAuthProvider, const BUF: usize = 4096> {
113    config: ConnectionConfig,
114    phase: ConnectionPhase,
115    activation: ActivationStateMachine<A>,
116    last_rx: Instant,
117    events: heapless::Vec<ConnectionEvent<BUF>, 8>,
118}
119
120impl<A: ActivationAuthProvider, const BUF: usize> ConnectionState<A, BUF> {
121    pub fn new(
122        config: ConnectionConfig,
123        activation: ActivationStateMachine<A>,
124        now: Instant,
125    ) -> Self {
126        Self {
127            config,
128            phase: ConnectionPhase::Connected,
129            activation,
130            last_rx: now,
131            events: heapless::Vec::new(),
132        }
133    }
134
135    // region: Public surface
136
137    pub fn is_active(&self) -> bool {
138        self.phase == ConnectionPhase::Active
139    }
140
141    pub fn active_source_address(&self) -> Option<u16> {
142        self.activation.state.active_source_address()
143    }
144
145    /// Processes a validated DoIP payload - raw bytes after the 8-byte header.
146    ///
147    /// The caller has already validated the DoIP header and decoded the payload type. This method
148    /// receives the payload bytes and the already-decoded payload type for dispatch.
149    pub fn handle(&mut self, payload_type: &PayloadType, payload_data: &[u8], now: Instant) {
150        self.last_rx = now;
151
152        match payload_type {
153            PayloadType::RoutingActivationRequest => self.on_routing_activation(payload_data),
154            PayloadType::DiagnosticMessage => self.on_diagnostic_message(payload_data),
155            PayloadType::AliveCheckRequest => self.on_alive_check_request(),
156            PayloadType::AliveCheckResponse => self.on_alive_check_response(),
157
158            // Other payload types - not handled at connection level
159            _ => {}
160        }
161    }
162
163    /// Advances connection timers - idle timeout and alive check timeout.
164    pub fn tick(&mut self, now: Instant) {
165        match &self.phase {
166            ConnectionPhase::Active => {
167                let idle = now
168                    .checked_duration_since(self.last_rx)
169                    .unwrap_or(Duration::ZERO);
170
171                if idle > self.config.idle_timeout {
172                    self.phase = ConnectionPhase::AliveCheckPending { sent_at: now };
173                    let _ = self.events.push(ConnectionEvent::SendAliveCheckRequest);
174                }
175            }
176            ConnectionPhase::AliveCheckPending { sent_at } => {
177                let elapsed = now
178                    .checked_duration_since(*sent_at)
179                    .unwrap_or(Duration::ZERO);
180
181                if elapsed > self.config.alive_check_timeout {
182                    self.phase = ConnectionPhase::Closing;
183                    let _ = self.events.push(ConnectionEvent::Close);
184                }
185            }
186            _ => {}
187        }
188    }
189
190    /// Drops the activation line - models ignition off or power loss mid-session.
191    pub fn drop_activation_line(&mut self, reason: ActivationDenialReason) {
192        self.activation.drop_line(reason);
193        self.phase = ConnectionPhase::Closing;
194        let _ = self.events.push(ConnectionEvent::Close);
195    }
196
197    /// Drains accumulated connection events.
198    pub fn drain_events(&mut self) -> impl Iterator<Item = ConnectionEvent<BUF>> + '_ {
199        self.events.drain(..)
200    }
201
202    // endregion: Public surface
203
204    // region: Frame handlers
205
206    fn on_routing_activation(&mut self, payload_data: &[u8]) {
207        let mut cursor = payload_data;
208        let req = match RoutingActivationRequest::decode(&mut cursor) {
209            Ok(r) => r,
210            Err(_) => {
211                // Malformed activation request - close connection
212                self.phase = ConnectionPhase::Closing;
213                let _ = self.events.push(ConnectionEvent::Close);
214                return;
215            }
216        };
217
218        let resp = self.activation.process_request(&req);
219
220        let _ = self
221            .events
222            .push(ConnectionEvent::SendActivationResponse(resp));
223
224        if self.activation.state.is_active() {
225            self.phase = ConnectionPhase::Active;
226        } else {
227            self.phase = ConnectionPhase::Closing;
228            let _ = self.events.push(ConnectionEvent::Close);
229        }
230    }
231
232    fn on_diagnostic_message(&mut self, payload_data: &[u8]) {
233        let source = u16::from_be_bytes([
234            payload_data.get(0).copied().unwrap_or(0),
235            payload_data.get(1).copied().unwrap_or(0),
236        ]);
237        let target = u16::from_be_bytes([
238            payload_data.get(2).copied().unwrap_or(0),
239            payload_data.get(3).copied().unwrap_or(0),
240        ]);
241
242        if !self.is_active() {
243            let _ = self.events.push(ConnectionEvent::SendDiagnosticNack {
244                source_address: source,
245                target_address: target,
246                nack_code: DiagnosticNackCode::InvalidSourceAddress,
247            });
248            return;
249        }
250
251        if self.activation.state.active_source_address() != Some(source) {
252            let _ = self.events.push(ConnectionEvent::SendDiagnosticNack {
253                source_address: source,
254                target_address: target,
255                nack_code: DiagnosticNackCode::InvalidSourceAddress,
256            });
257            return;
258        }
259
260        let uds_bytes = payload_data.get(4..).unwrap_or(&[]);
261
262        if uds_bytes.is_empty() {
263            let _ = self.events.push(ConnectionEvent::SendDiagnosticNack {
264                source_address: source,
265                target_address: target,
266                nack_code: DiagnosticNackCode::TransportProtocolError,
267            });
268            return;
269        }
270
271        let mut uds_data: heapless::Vec<u8, BUF> = heapless::Vec::new();
272
273        if uds_data.extend_from_slice(uds_bytes).is_err() {
274            let _ = self.events.push(ConnectionEvent::SendDiagnosticNack {
275                source_address: source,
276                target_address: target,
277                nack_code: DiagnosticNackCode::OutOfMemory,
278            });
279            return;
280        }
281
282        let _ = self.events.push(ConnectionEvent::SendDiagnosticAck {
283            source_address: source,
284            target_address: target,
285        });
286
287        let _ = self.events.push(ConnectionEvent::ForwardToEcu {
288            source_address: source,
289            target_address: target,
290            uds_data,
291        });
292    }
293
294    fn on_alive_check_request(&mut self) {
295        let _ = self.events.push(ConnectionEvent::SendAliveCheckResponse);
296    }
297
298    fn on_alive_check_response(&mut self) {
299        if matches!(self.phase, ConnectionPhase::AliveCheckPending { .. }) {
300            self.phase = ConnectionPhase::Active;
301        }
302    }
303
304    // endregion: Frame handlers
305}
306
307// endregion: ConnectionState