1/*
2 * SPDX-FileCopyrightText: 2025 Copyright (c) Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 */
13
14use std::{
15 sync::Arc,
16 time::{Duration, Instant},
17};
18
19use async_trait::async_trait;
20use cda_interfaces::{
21 DiagServiceError, DoipComParams, EcuAddresses, EcuConnectivityHandler, FunctionalTransport,
22 HashMap, HashMapExtensions, NetworkTopology, PhysicalTransport, RouteStatus, ServicePayload,
23 TransmissionParameters, TransportProbe, TransportResponse, VariantDetectionRequest,
24 VariantDetectionSender,
25 communication_control::{
26 GatewayLifecycle, TransportControl, TransportState, error::CommControlError,
27 },
28 dlt_ctx, pending_nrc_from_raw, uds_response_from_raw,
29 util::{self, tokio_ext},
30};
31use doip_definitions::{
32 header::ProtocolVersion,
33 payload::{
34 DiagnosticMessage, DiagnosticMessageNack, DoipPayload, GenericNack,
35 RoutingActivationRequest,
36 },
37};
38use futures::FutureExt;
39use tokio::{
40 sync::{Mutex, RwLock, broadcast, mpsc},
41 task::JoinHandle,
42};
43use tokio_util::sync::CancellationToken;
44
45pub mod config;
46pub mod error;
47pub use error::{ConnectionError, DoipGatewaySetupError};
48mod connection_receiver;
49mod connection_sender;
50mod connections;
51mod ecu_connection;
52pub mod socket;
53mod vir_vam;
54
55#[cfg(test)]
56pub(crate) mod test_helpers;
57
58use crate::{
59 config::DoipConfig,
60 connections::{EcuError, GatewayState},
61 socket::DoIPUdpSocket,
62};
63
64/// Timeout when suppressPosRspMsgIndicationBit is set.
65/// ECUs that reject a request usually respond quickly with an NRC and
66/// this timeout window should be able to capture those while
67/// not blocking for the full diagnostic timeout.
68///
69/// This constant is intentionally separate from `timeout_default` so it can be promoted to a
70/// configuration entry, or a `ComParam` in a future iteration without changing the
71/// surrounding logic.
72const SUPPRESS_POSITIVE_RESPONSE_TIMEOUT: Duration = Duration::from_millis(500);
73
74/// Events received on a `DoIP` connection.
75///
76/// `Msg` carries the raw UDS payload and its `DoIP` logical addresses. All other
77/// variants are ISO 13400-2 protocol-level events that must be handled before
78/// any UDS data is forwarded to the caller.
79#[derive(Debug, Clone)]
80enum DiagnosticResponse {
81 /// A UDS payload received from an ECU, carried verbatim before classification.
82 Msg {
83 source_address: u16,
84 target_address: u16,
85 data: Vec<u8>,
86 },
87 /// An acknowledgment of a previously sent UDS payload, carrying the original
88 /// request's logical address and data.
89 Ack((u16, Vec<u8>)),
90 /// A negative acknowledgment of a previously sent UDS payload, including
91 /// the original Nack message struct.
92 Nack(DiagnosticMessageNack),
93 /// A response to a `DoIP` alive-check request, indicating that the ECU is reachable.
94 AliveCheckResponse,
95 /// `TesterPresent` NRC - intercepted at the decoding layer so the receiver
96 /// can log-and-drop without routing it to a per-ECU channel.
97 TesterPresentNRC(u8),
98 /// Generic `DoIP` negative acknowledgment, carrying the `GenericNack` struct with the NACK code.
99 GenericNack(GenericNack), // todo #22 -> we need the address of the ECU that sent the nack
100}
101
102impl DiagnosticResponse {
103 fn matches_request(&self, request: &[u8]) -> bool {
104 let Some(sid) = request.first() else {
105 return false;
106 };
107
108 match self {
109 Self::Ack((_, previous)) => request.starts_with(previous),
110 // Positive and negative echoes both belong to the request: a
111 // final NRC can overtake the ACK, and ignoring it here consumes
112 // it from the receiver - the later response read then times out
113 // although the ECU answered.
114 Self::Msg { data, .. } => util::uds_response_matches_request_sid(*sid, data),
115 _ => false,
116 }
117 }
118}
119
120pub(crate) struct DoipGatewayState<T: EcuAddresses + DoipComParams> {
121 pub(crate) doip_connections: Arc<RwLock<Vec<Arc<DoipConnection>>>>,
122 pub(crate) logical_address_to_connection: Arc<RwLock<HashMap<u16, usize>>>,
123 pub(crate) ecus: Arc<HashMap<String, RwLock<T>>>,
124 /// `None` until the first successful `start()` binds it. No UDP socket is
125 /// created at construction time, only when an authorized activation runs.
126 pub(crate) socket: Arc<Mutex<Option<DoIPUdpSocket>>>,
127 pub(crate) netmask: u32,
128}
129
130/// Owns all connection tasks for one enabled lifecycle so they stop together.
131pub(crate) struct ConnectionTasks(Mutex<Vec<JoinHandle<()>>>);
132
133impl ConnectionTasks {
134 fn new() -> Self {
135 Self(Mutex::new(Vec::new()))
136 }
137
138 pub(crate) async fn push(&self, task: JoinHandle<()>) {
139 self.0.lock().await.push(task);
140 }
141
142 async fn shutdown(&self) {
143 let mut tasks = std::mem::take(&mut *self.0.lock().await);
144 for task in &tasks {
145 task.abort();
146 }
147 for task in tasks.drain(..) {
148 if let Err(error) = task.await
149 && error.is_panic()
150 {
151 tracing::error!(%error, "DoIP connection task panicked during shutdown");
152 }
153 }
154 }
155}
156
157async fn spawn_connection_task(
158 connection_tasks: &Arc<ConnectionTasks>,
159 name: &str,
160 task: impl Future<Output = ()> + Send + 'static,
161) {
162 let task = cda_interfaces::spawn_named!(name, task);
163 connection_tasks.push(task).await;
164}
165
166impl Drop for ConnectionTasks {
167 fn drop(&mut self) {
168 // Best effort cleanup, we cannot await the tasks in
169 // here. A user should call 'shutdown' instead of relying on this.
170 for task in self.0.get_mut().drain(..) {
171 tracing::warn!("DoIP connection tasks dropped without `shutdown`, aborting tasks");
172 task.abort();
173 }
174 }
175}
176
177impl<T: EcuAddresses + DoipComParams> Clone for DoipGatewayState<T> {
178 fn clone(&self) -> Self {
179 Self {
180 doip_connections: Arc::clone(&self.doip_connections),
181 logical_address_to_connection: Arc::clone(&self.logical_address_to_connection),
182 ecus: Arc::clone(&self.ecus),
183 socket: Arc::clone(&self.socket),
184 netmask: self.netmask,
185 }
186 }
187}
188
[docs] 189/// [[ dimpl~deferred-doip-transport, Deferred diagnostic transport activation, dimpl ]]
190pub struct DoipDiagGateway<T: EcuAddresses + DoipComParams> {
191 state: DoipGatewayState<T>,
192 config: DoipConfig,
193 variant_detection: VariantDetectionSender,
194 connectivity_handler: Arc<dyn EcuConnectivityHandler>,
195 lifecycle: Arc<GatewayLifecycle<DoipGatewayOperation>>,
196}
197
198/// Background work owned by the currently enabled lifecycle.
199#[derive(Default)]
200struct DoipGatewayOperation {
201 cancel: Option<CancellationToken>,
202 vam_listener: Option<JoinHandle<()>>,
203 connection_tasks: Option<Arc<ConnectionTasks>>,
204}
205
206impl Drop for DoipGatewayOperation {
207 fn drop(&mut self) {
208 if self.cancel.is_some() || self.vam_listener.is_some() || self.connection_tasks.is_some() {
209 tracing::warn!("DoIP gateway operation dropped without `shutdown`, aborting tasks");
210 }
211 self.take_and_abort();
212 }
213}
214
215impl DoipGatewayOperation {
216 fn take_and_abort(&mut self) -> (Option<JoinHandle<()>>, Option<Arc<ConnectionTasks>>) {
217 if let Some(cancel) = self.cancel.take() {
218 cancel.cancel();
219 }
220 let listener = self.vam_listener.take();
221 if let Some(listener) = &listener {
222 listener.abort();
223 }
224 (listener, self.connection_tasks.take())
225 }
226
227 async fn shutdown(&mut self) {
228 let (listener, connection_tasks) = self.take_and_abort();
229 if let Some(listener) = listener
230 && let Err(error) = listener.await
231 && !error.is_cancelled()
232 {
233 tracing::error!(%error, "DoIP VAM listener failed during shutdown");
234 }
235 if let Some(connection_tasks) = connection_tasks {
236 connection_tasks.shutdown().await;
237 }
238 }
239}
240
241/// A gateway discovered on the network during `DoIP` vehicle discovery.
242///
243/// Contains the IP address, ECU name, and logical address obtained from a
244/// `VehicleIdentificationResponse` or `EntityStatusResponse` message.
245#[derive(Debug)]
246pub(crate) struct DiscoveredGateway {
247 pub(crate) ip: String,
248 pub(crate) ecu_name: String,
249 pub(crate) logical_address: u16,
250 pub(crate) doip_protocol_version: ProtocolVersion,
251}
252
253/// Transport-level settings shared across all `DoIP` gateway connections.
254/// Built once from `DoipConfig` in `DoipDiagGateway::new`; cloned into each
255/// `GatewayDoipConfig` when a specific gateway is discovered.
256#[derive(Clone)]
257pub(crate) struct DoipTransportConfig {
258 /// IP address of the diagnostic tester interface.
259 pub(crate) tester_ip: String,
260 /// TCP port for `DoIP` communication.
261 pub(crate) port: u16,
262 /// TCP port for `DoIP` over TLS communication.
263 pub(crate) tls_port: u16,
264 /// Whether to send a `DiagnosticMessageAck` upon receiving a `DiagnosticMessage`.
265 pub(crate) send_diagnostic_message_ack: bool,
266 /// Timeout for sending a single `DoIP` message.
267 pub(crate) send_timeout: Duration,
268 /// Interval between alive-check requests on idle connections (0 = disabled).
269 pub(crate) alive_check_interval: Duration,
270}
271
272/// Per-gateway configuration combining the discovered gateway identity with the
273/// shared transport settings. Only constructed inside `handle_gateway_connection`
274/// once a real `ip` and `name` are known - never holds placeholder values.
275#[derive(Clone)]
276pub(crate) struct GatewayDoipConfig {
277 /// IP address of the gateway ECU.
278 pub(crate) gateway_ip: String,
279 /// Name of the gateway ECU.
280 pub(crate) name: String,
281 /// UDS address of the tester.
282 pub(crate) tester_address: [u8; 2],
283 /// The `DoIp` protocol version to use for this gateway connection.
284 /// Set from the protocol version field in the VAM.
285 pub(crate) protocol_version: ProtocolVersion,
286 /// Shared transport-level settings (tester IP, ports, socket config, timeouts).
287 pub(crate) transport: DoipTransportConfig,
288}
289
290/// ECU-lifecycle timeouts sourced from `EcuAddresses` / `DoipComParams` during gateway setup.
291#[derive(Clone, Copy)]
292pub(crate) struct EcuTimeouts {
293 pub(crate) routing_activation: Duration,
294 pub(crate) retry_delay: Duration,
295 pub(crate) connection: Duration,
296 pub(crate) max_retry_attempts: u32,
297}
298
299/// Parameters needed to (re-)establish a TCP connection to one `DoIP` gateway.
300/// Cloned into the reconnect task and passed to `ecu_connection` functions;
301/// does **not** carry one-time setup data such as the ECU list or variant-detection channel.
302#[derive(Clone)]
303pub(crate) struct GatewayConnectionConfig {
304 pub(crate) doip: GatewayDoipConfig,
305 pub(crate) routing_activation_request: RoutingActivationRequest,
306 pub(crate) ecu_timeouts: EcuTimeouts,
307}
308
309/// One-shot setup bundle consumed by `connection_handler` when bringing up a new gateway.
310/// Carries `GatewayConnectionConfig` plus the ECU list needed during initial channel creation
311/// and the `ConnectivityHandler` for propagating connection events.
312#[derive(Clone)]
313pub(crate) struct GatewaySetup {
314 pub(crate) connection: GatewayConnectionConfig,
315 pub(crate) ecus: Vec<u16>,
316 pub(crate) ecu_names: Vec<String>,
317 pub(crate) connectivity_handler: Arc<dyn EcuConnectivityHandler>,
318}
319
320struct DoipEcu {
321 sender: mpsc::Sender<DoipPayload>,
322 receiver: broadcast::Receiver<Result<DiagnosticResponse, EcuError>>,
323}
324
325struct DoipConnection {
326 ecus: HashMap<u16, Arc<Mutex<DoipEcu>>>,
327 ip: String,
328}
329
330impl<T: EcuAddresses + DoipComParams> DoipDiagGateway<T> {
331 /// Create a new `DoipDiagGateway` instance.
332 ///
333 /// Purely in-memory. No UDP socket is created or bound, no packet is sent
334 /// and no task is started, so this is safe to call in any `init_mode`.
335 ///
336 /// # Errors
337 /// Returns `String` if initialization fails, e.g. when the configured
338 /// tester address/subnet cannot be parsed into a netmask.
339 #[tracing::instrument(
340 skip(doip_config, ecus, variant_detection, connectivity_handler),
341 fields(
342 tester_ip = doip_config.tester_address,
343 gateway_port = doip_config.gateway_port,
344 ecu_count = ecus.len(),
345 dlt_context = dlt_ctx!("DOIP")
346 )
347 )]
348 pub async fn new(
349 doip_config: &DoipConfig,
350 ecus: Arc<HashMap<String, RwLock<T>>>,
351 variant_detection: VariantDetectionSender,
352 connectivity_handler: Arc<dyn EcuConnectivityHandler>,
353 ) -> Result<Self, DoipGatewaySetupError> {
354 Ok(Self {
355 state: DoipGatewayState {
356 doip_connections: Arc::new(RwLock::new(Vec::new())),
357 logical_address_to_connection: Arc::new(RwLock::new(HashMap::new())),
358 ecus,
359 socket: Arc::new(Mutex::new(None)),
360 netmask: create_netmask(&doip_config.tester_address, &doip_config.tester_subnet)?,
361 },
362 config: doip_config.clone(),
363 variant_detection,
364 connectivity_handler,
365 lifecycle: Arc::new(GatewayLifecycle::new(TransportState::Disabled)),
366 })
367 }
368
369 async fn start(
370 &self,
371 operation: &mut DoipGatewayOperation,
372 ) -> Result<(), DoipGatewaySetupError> {
373 let mut socket_guard = self.state.socket.lock().await;
374 if operation.cancel.is_some()
375 || operation.vam_listener.is_some()
376 || operation.connection_tasks.is_some()
377 || socket_guard.is_some()
378 {
379 return Err(DoipGatewaySetupError::InvalidState(
380 "DoIP communication is already running".to_string(),
381 ));
382 }
383
384 let config = &self.config;
385 let transport_config = DoipTransportConfig {
386 tester_ip: config.tester_address.clone(),
387 port: config.gateway_port,
388 tls_port: config.tls_port,
389 send_diagnostic_message_ack: config.send_diagnostic_message_ack,
390 send_timeout: Duration::from_millis(config.send_timeout_ms),
391 alive_check_interval: Duration::from_secs(config.alive_check_interval_secs),
392 };
393 let mask = self.state.netmask;
394 let cancel = CancellationToken::new();
395 let shutdown = cancel.child_token().cancelled_owned().shared();
396 let connection_tasks = Arc::new(ConnectionTasks::new());
397 operation.cancel = Some(cancel.clone());
398 operation.connection_tasks = Some(Arc::clone(&connection_tasks));
399
400 // TODO(persistence-init-mode): Implement as described in req~dt-ecu-list-persistence
401 let socket = socket_guard.insert(create_udp_vir_socket(
402 &config.tester_address,
403 config.gateway_port,
404 )?);
405 let gateways = vir_vam::get_vehicle_identification::<T, _>(
406 socket,
407 mask,
408 config.gateway_port,
409 &self.state.ecus,
410 shutdown.clone(),
411 )
412 .await
413 .map_err(|error| DoipGatewaySetupError::ResourceError(error.to_string()))?;
414 drop(socket_guard);
415
416 let mut gateway_ecu_map: HashMap<u16, Vec<u16>> = HashMap::new();
417 let mut gateway_ecu_name_map: HashMap<u16, Vec<String>> = HashMap::new();
418 for (ecu_name, ecu_lock) in self.state.ecus.iter() {
419 let ecu = ecu_lock.read().await;
420 gateway_ecu_map
421 .entry(ecu.logical_gateway_address())
422 .or_default()
423 .push(ecu.logical_address());
424 gateway_ecu_name_map
425 .entry(ecu.logical_gateway_address())
426 .or_default()
427 .push(ecu_name.clone());
428 }
429 for gateway in gateways {
430 let gateway_name = gateway.ecu_name.clone();
431 let gateway_ecu_names = gateway_ecu_name_map
432 .get(&gateway.logical_address)
433 .cloned()
434 .unwrap_or_default();
435 let variant_detection = self.variant_detection.clone();
436 let transport_config = transport_config.clone();
437 let doip_connections = Arc::clone(&self.state.doip_connections);
438 let logical_address_to_connection =
439 Arc::clone(&self.state.logical_address_to_connection);
440 let gateway_state = GatewayState {
441 doip_connections: Arc::clone(&doip_connections),
442 ecus: Arc::clone(&self.state.ecus),
443 gateway_ecu_map: gateway_ecu_map.clone(),
444 connection_tasks: Arc::clone(&connection_tasks),
445 };
446 let connectivity_handler = Arc::clone(&self.connectivity_handler);
447 spawn_connection_task(
448 &connection_tasks,
449 &format!("doip-gateway-connect-{gateway_name}"),
450 async move {
451 match connections::handle_gateway_connection::<T>(
452 gateway,
453 &transport_config,
454 &gateway_state,
455 connectivity_handler,
456 )
457 .await
458 {
459 Ok(logical_address) => {
460 let connection_index =
461 doip_connections.read().await.iter().position(|connection| {
462 connection.ecus.contains_key(&logical_address)
463 });
464 if let Some(connection_index) = connection_index {
465 logical_address_to_connection
466 .write()
467 .await
468 .insert(logical_address, connection_index);
469 }
470 if let Err(error) = variant_detection
471 .send(VariantDetectionRequest::new(gateway_ecu_names))
472 .await
473 {
474 tracing::warn!(
475 gateway = %gateway_name,
476 error = ?error,
477 "Failed to trigger variant detection after gateway connection"
478 );
479 }
480 }
481 Err(error) => {
482 tracing::warn!(
483 gateway = %gateway_name,
484 error = %error,
485 "Failed to establish initial gateway connection"
486 );
487 }
488 }
489 },
490 )
491 .await;
492 }
493
494 let listener = vir_vam::listen_for_vams(
495 transport_config,
496 mask,
497 self.state.clone(),
498 Arc::clone(&connection_tasks),
499 self.variant_detection.clone(),
500 Arc::clone(&self.connectivity_handler),
501 shutdown,
502 )
503 .await;
504 operation.vam_listener = Some(listener);
505 Ok(())
506 }
507
508 async fn stop(&self, operation: &mut DoipGatewayOperation) {
509 operation.shutdown().await;
510 self.state.socket.lock().await.take();
511 self.state.doip_connections.write().await.clear();
512 self.state
513 .logical_address_to_connection
514 .write()
515 .await
516 .clear();
517 }
518
519 async fn get_doip_connection(
520 &self,
521 logical_address: u16,
522 ) -> Result<Arc<DoipConnection>, DiagServiceError> {
523 let conn_idx = *self
524 .state
525 .logical_address_to_connection
526 .read()
527 .await
528 .get(&logical_address)
529 .ok_or_else(|| DiagServiceError::EcuOffline(format!("[{logical_address}]")))?;
530
531 let lock = self.state.doip_connections.read().await;
532 let conn = lock
533 .get(conn_idx)
534 .ok_or(DiagServiceError::ConnectionClosed(format!(
535 "Connection entry for address {logical_address} found, but it was already closed"
536 )))?;
537
538 Ok(Arc::clone(conn))
539 }
540
541 async fn get_ecu_mtx(
542 &self,
543 doip_conn: &DoipConnection,
544 message: &ServicePayload,
545 transmission_params: &TransmissionParameters,
546 ) -> Result<Arc<Mutex<DoipEcu>>, DiagServiceError> {
547 // first try looking up with the target address.
548 if let Some(ecu) = doip_conn.ecus.get(&message.target_address) {
549 return Ok(Arc::clone(ecu));
550 }
551
552 // if we cannot find the target address,
553 // the request might be sent on the functional address
554 // in that case, lookup the ecu name and check if the functional address
555 // matches the given address.
556 // this will be the case for tester present.
557 if let Some(ecu) = self
558 .state
559 .ecus
560 .get(&transmission_params.ecu_name.to_lowercase())
561 && ecu.read().await.logical_functional_address() == message.target_address
562 && let Some(gateway_ecu) = doip_conn.ecus.get(&transmission_params.gateway_address)
563 {
564 return Ok(Arc::clone(gateway_ecu));
565 }
566
567 Err(DiagServiceError::EcuOffline(
568 transmission_params.ecu_name.clone(),
569 ))
570 }
571}
572
573impl<T: EcuAddresses + DoipComParams> PhysicalTransport for DoipDiagGateway<T> {
574 #[tracing::instrument(skip_all,
575 fields(dlt_context = dlt_ctx!("DOIP"))
576 )]
577
578 async fn send(
579 &self,
580 transmission_params: TransmissionParameters,
581 message: ServicePayload,
582 response_sender: mpsc::Sender<Result<Option<TransportResponse>, DiagServiceError>>,
583 expect_uds_reply: bool,
584 ) -> Result<tokio::task::JoinHandle<()>, DiagServiceError> {
585 let start = Instant::now();
586
587 let doip_conn = self
588 .get_doip_connection(transmission_params.gateway_address)
589 .await?;
590 let ecu_mtx = self
591 .get_ecu_mtx(&doip_conn, &message, &transmission_params)
592 .await?;
593
594 let doip_message = DiagnosticMessage {
595 source_address: message.source_address.to_be_bytes(),
596 target_address: message.target_address.to_be_bytes(),
597 message: message.data,
598 };
599
600 let handle = cda_interfaces::spawn_named!(
601 &format!("ecu-data-receive-{}", transmission_params.ecu_name),
602 {
603 async move {
604 let mut ecu = ecu_mtx.lock().await;
605 let lock_acquired = start.elapsed();
606 tracing::debug!(
607 ecu_name = %transmission_params.ecu_name,
608 locked_after = ?lock_acquired,
609 message_data = %util::tracing::print_hex(&doip_message.message, 8),
610 "Sending Message to ECU"
611 );
612
613 // Clear any pending messages
614 tokio_ext::clear_pending_messages(&mut ecu.receiver);
615 let receiver_flushed = start.elapsed().saturating_sub(lock_acquired);
616
617 let mut resend_counter = 0;
618 if let Err(e) = send_with_retries(
619 &doip_message,
620 &ecu.sender,
621 &mut resend_counter,
622 transmission_params.repeat_request_count_transmission,
623 )
624 .await
625 {
626 // failed to send the message after exhausting retries.
627 // informing receiver and giving up.
628 try_send_transport_response(&response_sender, Err(e)).await;
629 return;
630 }
631
632 let received_event = match wait_for_ack_or_response_until_timeout(
633 &mut ecu.receiver,
634 &transmission_params.ecu_name,
635 &doip_message,
636 transmission_params.timeout_ack,
637 )
638 .await
639 {
640 Ok(first) => first,
641 Err(e) => {
642 try_send_transport_response(&response_sender, Err(e)).await;
643 return;
644 }
645 };
646
647 let send_and_ackd_after = start
648 .elapsed()
649 .saturating_sub(lock_acquired)
650 .saturating_sub(receiver_flushed);
651
652 if !expect_uds_reply {
653 try_send_transport_response(&response_sender, Ok(None)).await;
654 }
655
656 // Read ECU responses as long as the sender is open.
657 // We might get multiple responses for a single request,
658 // e.g. when the ECU is busy and sends NRC 0x78.
659 read_ecu_responses(
660 &mut ecu.receiver,
661 &transmission_params.ecu_name,
662 &response_sender,
663 received_event,
664 )
665 .await;
666
667 let rx_done = start
668 .elapsed()
669 .saturating_sub(lock_acquired)
670 .saturating_sub(send_and_ackd_after)
671 .saturating_sub(receiver_flushed);
672 tracing::debug!(
673 ecu_name = %transmission_params.ecu_name,
674 total_duration = ?start.elapsed(),
675 lock_duration = ?lock_acquired,
676 flush_duration = ?receiver_flushed,
677 send_ack_duration = ?send_and_ackd_after,
678 response_duration = ?rx_done,
679 "Handled DOIP request timing breakdown"
680 );
681 }
682 }
683 );
684
685 Ok(handle)
686 }
687
688 async fn ecu_online<E: EcuAddresses>(
689 &self,
690 ecu_name: &str,
691 ecu_db: &RwLock<E>,
692 ) -> Result<(), DiagServiceError> {
693 let ecu_lock = ecu_db.read().await;
694
695 let doip_conn = self
696 .get_doip_connection(ecu_lock.logical_gateway_address())
697 .await?;
698 doip_conn
699 .ecus
700 .get(&ecu_lock.logical_address())
701 .ok_or_else(|| DiagServiceError::EcuOffline(ecu_name.to_owned()))?;
702 Ok(())
703 }
704}
705
706impl<T: EcuAddresses + DoipComParams> FunctionalTransport for DoipDiagGateway<T> {
707 async fn send_functional(
708 &self,
709 transmission_params: TransmissionParameters,
710 message: ServicePayload,
711 expected_ecu_logical_addrs: HashMap<u16, String>,
712 timeout: Duration,
713 expect_positive_response: bool,
714 ) -> Result<HashMap<String, Result<ServicePayload, DiagServiceError>>, DiagServiceError> {
715 let doip_conn = self
716 .get_doip_connection(transmission_params.gateway_address)
717 .await?;
718
719 // Get the gateway ECU for sending the functional request
720 let gateway_ecu = doip_conn
721 .ecus
722 .get(&transmission_params.gateway_address)
723 .ok_or_else(|| DiagServiceError::EcuOffline("Gateway ECU not found".to_string()))?;
724
725 let doip_message = DiagnosticMessage {
726 source_address: message.source_address.to_be_bytes(),
727 target_address: message.target_address.to_be_bytes(),
728 message: message.data,
729 };
730
731 let mut result_map = HashMap::new();
732 let expected_count = expected_ecu_logical_addrs.len();
733
734 tracing::debug!(
735 gateway_address = %transmission_params.gateway_address,
736 expected_ecus = expected_count,
737 message_data = %util::tracing::print_hex(&doip_message.message, 8),
738 "Sending functional request to gateway"
739 );
740
741 // Send the functional request once
742 let mut ecu = gateway_ecu.lock().await;
743 let mut ecu_mtxs = expected_ecu_logical_addrs
744 .iter()
745 .filter_map(|(addr, name)| {
746 if *addr == transmission_params.gateway_address {
747 None
748 } else {
749 doip_conn
750 .ecus
751 .get(addr)
752 .cloned()
753 .map(|ecu| (name.clone(), ecu))
754 }
755 })
756 .collect::<Vec<_>>();
757
758 // Clear any pending messages
759 tokio_ext::clear_pending_messages(&mut ecu.receiver);
760
761 let mut resend_counter = 0;
762 send_with_retries(
763 &doip_message,
764 &ecu.sender,
765 &mut resend_counter,
766 transmission_params.repeat_request_count_transmission,
767 )
768 .await?;
769
770 drop(ecu); // release lock before waiting for responses
771 ecu_mtxs.push((
772 transmission_params.ecu_name.to_lowercase(),
773 Arc::clone(gateway_ecu),
774 ));
775
776 // Use a short window to capture any negative responses instead of the full timeout,
777 // as NRCs are usually sent immediately by the ECUs.
778 let response_timeout = if expect_positive_response {
779 timeout
780 } else {
781 SUPPRESS_POSITIVE_RESPONSE_TIMEOUT
782 };
783
784 let received_responses: Arc<Mutex<HashMap<String, Result<DiagnosticMessage, EcuError>>>> =
785 Arc::new(Mutex::new(HashMap::new()));
786 let mut futures = Vec::new();
787 for (name, ecu) in ecu_mtxs.drain(..) {
788 let received_responses = Arc::clone(&received_responses);
789 let fut = async move {
790 let mut lock = ecu.lock().await;
791 if let Some(response) = wait_for_ecu_response(&mut lock, response_timeout).await {
792 received_responses.lock().await.insert(name, response);
793 }
794 };
795 futures.push(fut);
796 }
797
798 futures::future::join_all(futures).await;
799
800 for (ecu_name, msg) in received_responses.lock().await.drain() {
801 if !result_map.contains_key(&ecu_name) {
802 match msg {
803 Ok(msg) => {
804 let source_addr = u16::from_be_bytes(msg.source_address);
805
806 let uds_response = ServicePayload {
807 data: msg.message,
808 source_address: source_addr,
809 target_address: u16::from_be_bytes(msg.target_address),
810 new_session: None,
811 new_security: None,
812 };
813
814 result_map.insert(ecu_name.clone(), Ok(uds_response));
815
816 tracing::debug!(
817 ecu_name = %ecu_name,
818 source_addr = source_addr,
819 "Received functional response"
820 );
821 }
822 Err(e) => {
823 tracing::debug!(
824 ecu_name = %ecu_name,
825 "Error receiving functional response: {e}"
826 );
827 result_map.insert(ecu_name.clone(), Err(e.into()));
828 }
829 }
830 }
831 }
832
833 // For ECUs that did not respond:
834 // Insert a TimeoutError for ECUs that did not response, if
835 // a positive response is expected. (suppress bit is not set)
836 for (logical_addr, ecu_name) in &expected_ecu_logical_addrs {
837 if !result_map.contains_key(ecu_name) && expect_positive_response {
838 result_map.insert(ecu_name.clone(), Err(DiagServiceError::Timeout));
839 tracing::debug!(
840 ecu_name = %ecu_name,
841 logical_addr = logical_addr,
842 "ECU did not respond to functional request"
843 );
844 }
845 }
846
847 Ok(result_map)
848 }
849}
850
851impl<T: EcuAddresses + DoipComParams> NetworkTopology for DoipDiagGateway<T> {
852 async fn get_gateway_network_address(&self, logical_address: u16) -> Option<String> {
853 self.state
854 .doip_connections
855 .read()
856 .await
857 .iter()
858 .find(|conn| conn.ecus.contains_key(&logical_address))
859 .map(|conn| conn.ip.clone())
860 }
861}
862
863impl<T: EcuAddresses + DoipComParams> TransportProbe for DoipDiagGateway<T> {
864 async fn route_status(&self, ecu_name: &str) -> RouteStatus {
865 let ecu_name = ecu_name.to_lowercase();
866 let Some(ecu_lock) = self.state.ecus.get(&ecu_name) else {
867 return RouteStatus::NotConfigured;
868 };
869 // Check if the gateway connection for this ECU is established
870 let gateway_addr = ecu_lock.read().await.logical_gateway_address();
871 if self.get_doip_connection(gateway_addr).await.is_ok() {
872 RouteStatus::Ready
873 } else {
874 // DoIP addressing is gateway-scoped, so there is
875 // no per-ECU probe to offer. Reconnecting is this transport's job
876 // (VAM listener, connection-reset task), not the router's.
877 RouteStatus::Unavailable
878 }
879 }
880
881 fn probe_ecu(&self, _ecu_name: &str) -> impl Future<Output = bool> {
882 std::future::ready(false)
883 }
884}
885/// Waits for the `DoIP` diagnostic-message acknowledgement from the gateway,
886/// with a deadline of `timeout`.
887#[allow(
888 clippy::needless_continue,
889 reason = "Explicit continue improves readability of wait logic"
890)]
891async fn wait_for_ack_or_response_until_timeout(
892 receiver: &mut broadcast::Receiver<Result<DiagnosticResponse, EcuError>>,
893 ecu_name: &str,
894 sent_message: &DiagnosticMessage,
895 timeout: Duration,
896) -> Result<Option<DiagnosticResponse>, DiagServiceError> {
897 async fn wait_for_ack_or_response(
898 receiver: &mut broadcast::Receiver<Result<DiagnosticResponse, EcuError>>,
899 ecu_name: &str,
900 sent_message: &DiagnosticMessage,
901 ) -> Result<Option<DiagnosticResponse>, DiagServiceError> {
902 loop {
903 let response = match receiver.recv().await {
904 Ok(Ok(response)) => response,
905 Ok(Err(e)) => {
906 tracing::error!(
907 ecu_name = %ecu_name,
908 error = %e,
909 "Error while waiting for ACK/NACK"
910 );
911
912 return Err(DiagServiceError::NoResponse(format!(
913 "Error while waiting for ACK/NACK, {e}"
914 )));
915 }
916 Err(_) => {
917 tracing::error!(
918 ecu_name = %ecu_name,
919 "ECU receiver unexpectedly closed while waiting for ACK/NACK"
920 );
921
922 return Err(DiagServiceError::NoResponse(
923 "ECU receiver unexpectedly closed".to_owned(),
924 ));
925 }
926 };
927
928 match &response {
929 DiagnosticResponse::Nack(nack) => {
930 tracing::warn!(
931 ecu_name = %ecu_name,
932 nack_code = ?nack.nack_code,
933 "Received NACK"
934 );
935 return Err(DiagServiceError::Nack(u8::from(nack.nack_code)));
936 }
937 DiagnosticResponse::GenericNack(nack) => {
938 tracing::warn!(
939 ecu_name = %ecu_name,
940 nack_code = ?nack.nack_code,
941 "Received generic NACK"
942 );
943 return Err(DiagServiceError::Nack(u8::from(nack.nack_code)));
944 }
945 _ => {}
946 }
947
948 if !response.matches_request(&sent_message.message) {
949 if let DiagnosticResponse::Ack((_, previous)) = &response {
950 tracing::warn!(
951 ecu_name = %ecu_name,
952 previous = %util::tracing::print_hex(previous, 8),
953 sent = %util::tracing::print_hex(&sent_message.message, 8),
954 "ACK previous message does not match sent message"
955 );
956 } else {
957 tracing::debug!(
958 ecu_name = %ecu_name,
959 ?response,
960 "Received response does not match sent message. Ignoring."
961 );
962 }
963
964 continue;
965 }
966
967 return match response {
968 DiagnosticResponse::Ack((_, _)) => {
969 tracing::debug!(ecu_name = %ecu_name, "Received ACK");
970 Ok(None)
971 }
972 response => {
973 tracing::debug!(
974 ecu_name = %ecu_name,
975 "Received diagnostic response before ACK, treating as implicit ACK"
976 );
977 Ok(Some(response))
978 }
979 };
980 }
981 }
982
983 if let Ok(result) = tokio::time::timeout(
984 timeout,
985 wait_for_ack_or_response(receiver, ecu_name, sent_message),
986 )
987 .await
988 {
989 result
990 } else {
991 tracing::warn!(
992 ecu_name = %ecu_name,
993 timeout = ?timeout,
994 "Timeout waiting for ACK/NACK from ECU"
995 );
996 Err(DiagServiceError::Timeout)
997 }
998}
999
1000/// Reads ECU responses from `receiver` and forwards them through
1001/// `response_sender` until either the sender is closed (caller no longer
1002/// interested) or the receiver is closed (connection dropped).
1003///
1004/// `received_message` carries an already-received response from the implicit-ACK path
1005/// (where the ECU skipped the ACK and sent the diagnostic reply directly).
1006/// Pass `None` after a normal explicit ACK.
1007async fn read_ecu_responses(
1008 receiver: &mut broadcast::Receiver<Result<DiagnosticResponse, EcuError>>,
1009 ecu_name: &str,
1010 response_sender: &mpsc::Sender<Result<Option<TransportResponse>, DiagServiceError>>,
1011 received_event: Option<DiagnosticResponse>,
1012) {
1013 if let Some(event) = received_event
1014 && !try_send_transport_response(response_sender, diagnostic_response_to_transport(event))
1015 .await
1016 {
1017 return;
1018 }
1019 loop {
1020 tokio::select! {
1021 // Using biased saves a bit of CPU time because tokio does not
1022 // have to generate a random number to select the branch.
1023 // Prioritizing the ECU receiver over the closed handler is fine
1024 // because it is unlikely that both fire at the exact same time.
1025 biased;
1026 res = receiver.recv() => {
1027 if let Ok(res) = res {
1028 match res {
1029 Ok(event) => {
1030 if !try_send_transport_response(
1031 response_sender,
1032 diagnostic_response_to_transport(event),
1033 )
1034 .await
1035 {
1036 break;
1037 }
1038 }
1039 Err(e) => {
1040 tracing::error!(
1041 ecu_name = %ecu_name,
1042 error = %e,
1043 "Error while waiting for response message"
1044 );
1045 if !try_send_transport_response(
1046 response_sender,
1047 Err(DiagServiceError::NoResponse(format!(
1048 "Error while waiting for message, {e}"
1049 ))),
1050 )
1051 .await
1052 {
1053 break;
1054 }
1055 }
1056 }
1057 } else {
1058 tracing::error!(
1059 ecu_name = %ecu_name,
1060 "ECU receiver unexpectedly closed while waiting for response"
1061 );
1062 try_send_transport_response(
1063 response_sender,
1064 Err(DiagServiceError::NoResponse(
1065 "ECU receiver unexpectedly closed".to_owned(),
1066 )),
1067 )
1068 .await;
1069 break;
1070 }
1071 }
1072 () = response_sender.closed() => {
1073 tracing::debug!(
1074 ecu_name = %ecu_name,
1075 "Response sender closed, aborting loop"
1076 );
1077 break;
1078 }
1079 }
1080 }
1081}
1082
1083/// Converts a [`DiagnosticResponse::Msg`] to a [`TransportResponse`] result for the
1084/// caller channel.
1085///
1086/// Pending NRCs are classified via [`pending_nrc_from_raw`]; all other frames
1087/// are classified as final via [`uds_response_from_raw`].
1088///
1089/// `TesterPresentNRC` is absorbed by [`connection_receiver`] before it is
1090/// broadcast to any per-ECU channel -- it never reaches this function.
1091/// All other [`DiagnosticResponse`] variants (Ack, Nack, etc.) must be
1092/// handled before calling this function.
1093fn diagnostic_response_to_transport(
1094 event: DiagnosticResponse,
1095) -> Result<Option<TransportResponse>, DiagServiceError> {
1096 match event {
1097 DiagnosticResponse::Msg {
1098 data,
1099 source_address,
1100 target_address,
1101 } => {
1102 if let Some(pending) = pending_nrc_from_raw(&data, source_address) {
1103 Ok(Some(TransportResponse::Pending(pending)))
1104 } else {
1105 Ok(Some(TransportResponse::UdsResponse(uds_response_from_raw(
1106 data,
1107 source_address,
1108 target_address,
1109 ))))
1110 }
1111 }
1112 _ => Err(DiagServiceError::BadPayload(
1113 "Unexpected DoIP event type in UDS response stream".to_owned(),
1114 )),
1115 }
1116}
1117
1118#[allow(
1119 clippy::needless_continue,
1120 reason = "Explicit continue improves readability of complex loop logic"
1121)]
1122async fn wait_for_ecu_response(
1123 ecu: &mut DoipEcu,
1124 timeout: Duration,
1125) -> Option<Result<DiagnosticMessage, EcuError>> {
1126 tokio::time::timeout(timeout, async {
1127 loop {
1128 match ecu.receiver.recv().await {
1129 Ok(Ok(DiagnosticResponse::Msg {
1130 source_address,
1131 target_address,
1132 data,
1133 })) => {
1134 return Some(Ok(DiagnosticMessage {
1135 source_address: source_address.to_be_bytes(),
1136 target_address: target_address.to_be_bytes(),
1137 message: data,
1138 }));
1139 }
1140 Ok(Ok(_ignore)) => {
1141 // Ignore other event types
1142 continue;
1143 }
1144 Ok(Err(e)) => {
1145 return Some(Err(e));
1146 }
1147 Err(_) => {
1148 // Receiver closed
1149 return None;
1150 }
1151 }
1152 }
1153 })
1154 .await
1155 .unwrap_or_default()
1156}
1157
1158fn create_netmask(tester_ip: &str, tester_subnet: &str) -> Result<u32, DoipGatewaySetupError> {
1159 let ip = tester_ip.parse::<std::net::Ipv4Addr>().map_err(|e| {
1160 DoipGatewaySetupError::InvalidAddress(format!(
1161 "DoipGateway: Failed to parse tester IP address: {e:?}"
1162 ))
1163 })?;
1164 let subnet = tester_subnet.parse::<std::net::Ipv4Addr>().map_err(|e| {
1165 DoipGatewaySetupError::InvalidAddress(format!(
1166 "DoipGateway: Failed to parse tester subnet mask: {e:?}"
1167 ))
1168 })?;
1169
1170 Ok(ip.to_bits() & subnet.to_bits())
1171}
1172
1173/// Creates a UDP socket for `DoIP` communication.
1174///
1175/// # Errors
1176/// Returns errors when:
1177/// * The provided `tester_ip` and `gateway_port` cannot be parsed into a valid `SocketAddr`,
1178/// resulting in a `DoipGatewaySetupError::InvalidAddress`.
1179/// * The underlying system call to create a new socket fails,
1180/// resulting in a `DoipGatewaySetupError::SocketCreationFailed`.
1181/// * Setting the `SO_REUSEADDR` socket option fails,
1182/// resulting in a `DoipGatewaySetupError::InvalidAddress`.
1183/// * On Unix-like systems, setting the `SO_REUSEPORT` socket option fails,
1184/// resulting in a `DoipGatewaySetupError::PortBindFailed`.
1185/// * Setting the `SO_BROADCAST` socket option fails,
1186/// resulting in a `DoipGatewaySetupError::SocketCreationFailed`.
1187/// * Binding the socket to the specified `tester_ip` and `gateway_port` fails,
1188/// resulting in a `DoipGatewaySetupError::SocketCreationFailed`.
1189/// * The `DoIPUdpSocket` constructor fails to create the DoIP-specific socket from the standard UDP socket,
1190/// resulting in a `DoipGatewaySetupError::SocketCreationFailed`.
1191pub fn create_udp_vir_socket(
1192 tester_ip: &str,
1193 gateway_port: u16,
1194) -> Result<DoIPUdpSocket, DoipGatewaySetupError> {
1195 let tester_ip = match tester_ip {
1196 "127.0.0.1" => "0.0.0.0",
1197 _ => tester_ip,
1198 };
1199 let broadcast_addr: std::net::SocketAddr =
1200 format!("{tester_ip}:{gateway_port}").parse().map_err(|e| {
1201 DoipGatewaySetupError::InvalidAddress(format!(
1202 "DoipGateway: Failed to create broadcast addr: {e:?}"
1203 ))
1204 })?;
1205
1206 let socket = socket2::Socket::new(
1207 socket2::Domain::IPV4,
1208 socket2::Type::DGRAM,
1209 Some(socket2::Protocol::UDP),
1210 )
1211 .map_err(|e| {
1212 DoipGatewaySetupError::SocketCreationFailed(format!(
1213 "DoipGateway: Failed to create socket: {e:?}"
1214 ))
1215 })?;
1216
1217 socket.set_reuse_address(true).map_err(|e| {
1218 DoipGatewaySetupError::InvalidAddress(format!(
1219 "DoipGateway: Failed to set reuse address: {e:?}"
1220 ))
1221 })?;
1222 #[cfg(target_family = "unix")]
1223 socket.set_reuse_port(true).map_err(|e| {
1224 DoipGatewaySetupError::PortBindFailed(format!(
1225 "DoipGateway: Failed to set reuse port: {e:?}"
1226 ))
1227 })?;
1228 socket.set_broadcast(true).map_err(|e| {
1229 DoipGatewaySetupError::SocketCreationFailed(format!(
1230 "DoipGateway: Failed to set broadcast flag on socket: {e:?}"
1231 ))
1232 })?;
1233 socket.set_nonblocking(true).map_err(|e| {
1234 DoipGatewaySetupError::InvalidConfiguration(format!(
1235 "DoipGateway: Failed to set non-blocking mode: {e:?}"
1236 ))
1237 })?;
1238
1239 socket.bind(&broadcast_addr.into()).map_err(|e| {
1240 DoipGatewaySetupError::SocketCreationFailed(format!(
1241 "DoipGateway: Failed to bind socket, ip {tester_ip}, port {gateway_port}: {e:?}"
1242 ))
1243 })?;
1244
1245 let std_sock: std::net::UdpSocket = socket.into();
1246 DoIPUdpSocket::new(std_sock, ProtocolVersion::DefaultValue).map_err(|e| {
1247 DoipGatewaySetupError::SocketCreationFailed(format!(
1248 "DoipGateway: Failed to create DoIP socket from std socket: {e:?}"
1249 ))
1250 })
1251}
1252
1253impl<T: EcuAddresses + DoipComParams> Clone for DoipDiagGateway<T> {
1254 fn clone(&self) -> Self {
1255 Self {
1256 state: self.state.clone(),
1257 config: self.config.clone(),
1258 variant_detection: self.variant_detection.clone(),
1259 connectivity_handler: Arc::clone(&self.connectivity_handler),
1260 lifecycle: Arc::clone(&self.lifecycle),
1261 }
1262 }
1263}
1264
1265#[async_trait]
1266impl<T: EcuAddresses + DoipComParams> cda_interfaces::Shutdown for DoipDiagGateway<T> {
1267 async fn shutdown(&self) {
1268 if let Err(error) = self.disable().await {
1269 tracing::error!(%error, "Failed to disable DoIP communication during shutdown");
1270 }
1271 }
1272}
1273
1274#[async_trait]
1275impl<T: EcuAddresses + DoipComParams> TransportControl for DoipDiagGateway<T> {
1276 async fn enable(&self) -> Result<(), CommControlError> {
1277 let mut operation = self.lifecycle.operation.lock().await;
1278 if self.lifecycle.coordinator.active().await {
1279 return Ok(());
1280 }
1281 // Clean up any residual resources, usually should be a no-op
1282 self.stop(&mut operation).await;
1283 self.lifecycle
1284 .coordinator
1285 .transition(TransportState::Enabling)
1286 .await;
1287 match self.start(&mut operation).await {
1288 Ok(()) => {
1289 self.lifecycle
1290 .coordinator
1291 .transition(TransportState::Enabled)
1292 .await;
1293 Ok(())
1294 }
1295 Err(error) => {
1296 self.stop(&mut operation).await;
1297 self.lifecycle
1298 .coordinator
1299 .transition(TransportState::Failed)
1300 .await;
1301 Err(CommControlError::InitFailed(error.to_string()))
1302 }
1303 }
1304 }
1305
1306 async fn disable(&self) -> Result<(), CommControlError> {
1307 let mut operation = self.lifecycle.operation.lock().await;
1308 self.lifecycle
1309 .coordinator
1310 .transition(TransportState::Disabling)
1311 .await;
1312 self.stop(&mut operation).await;
1313 self.lifecycle
1314 .coordinator
1315 .transition(TransportState::Disabled)
1316 .await;
1317 Ok(())
1318 }
1319
1320 async fn state(&self) -> TransportState {
1321 self.lifecycle.coordinator.state().await
1322 }
1323}
1324
1325async fn send_with_retries(
1326 msg: &DiagnosticMessage,
1327 sender: &mpsc::Sender<DoipPayload>,
1328 resend_counter: &mut u32,
1329 max_retries: u32,
1330) -> Result<(), DiagServiceError> {
1331 while let Err(e) = sender
1332 .send(DoipPayload::DiagnosticMessage(msg.clone()))
1333 .await
1334 {
1335 *resend_counter = resend_counter.saturating_add(1);
1336 if *resend_counter > max_retries {
1337 return Err(DiagServiceError::SendFailed(format!(
1338 "Failed to send message after {max_retries} attempts: {e:?}",
1339 )));
1340 }
1341 }
1342 Ok(())
1343}
1344
1345#[tracing::instrument(skip_all,
1346 fields(dlt_context = dlt_ctx!("DOIP"))
1347)]
1348async fn try_send_transport_response(
1349 response_sender: &mpsc::Sender<Result<Option<TransportResponse>, DiagServiceError>>,
1350 response: Result<Option<TransportResponse>, DiagServiceError>,
1351) -> bool {
1352 if let Err(err) = response_sender.send(response).await {
1353 tracing::error!(error = %err, "Failed to send response");
1354 return false;
1355 }
1356 true
1357}
1358
1359#[cfg(test)]
1360mod tests {
1361 use std::{net::UdpSocket, sync::Arc, time::Duration};
1362
1363 use cda_interfaces::{
1364 DiagServiceError, EcuConnectivityHandler, HashMap, HashMapExtensions, PendingNrc,
1365 PhysicalTransport, ServicePayload, TransmissionParameters, TransportResponse,
1366 UDS_ID_RESPONSE_BITMASK, VariantDetectionSender,
1367 communication_control::{GatewayLifecycle, TransportState},
1368 nrc, service_ids,
1369 };
1370 use doip_definitions::{
1371 header::ProtocolVersion,
1372 payload::{DiagnosticMessage, DoipPayload},
1373 };
1374 use tokio::sync::{Mutex, RwLock, broadcast, mpsc};
1375
1376 use crate::{
1377 ConnectionTasks, DiagnosticResponse, DoIPUdpSocket, DoipConfig, DoipConnection,
1378 DoipDiagGateway, DoipEcu, DoipGatewayState, read_ecu_responses, spawn_connection_task,
1379 test_helpers::TestEcu, wait_for_ack_or_response_until_timeout,
1380 };
1381
1382 const ECU_ADDR: u16 = 0x0E80;
1383 const GATEWAY_ADDR: u16 = 0x1234;
1384 const TESTER_ADDR: u16 = 0x0E00;
1385 const PAYLOAD: [u8; 2] = [0xF1, 0x90];
1386 const REQUEST_DATA: [u8; 3] = [service_ids::READ_DATA_BY_IDENTIFIER, PAYLOAD[0], PAYLOAD[1]];
1387 const RESPONSE_DATA: [u8; 4] = [
1388 service_ids::READ_DATA_BY_IDENTIFIER | UDS_ID_RESPONSE_BITMASK,
1389 PAYLOAD[0],
1390 PAYLOAD[1],
1391 0x01,
1392 ];
1393
1394 struct TestConnectivityHandler;
1395
1396 #[async_trait::async_trait]
1397 impl EcuConnectivityHandler for TestConnectivityHandler {
1398 async fn on_gateway_connected(&self, _ecu_names: &[String]) {}
1399
1400 async fn on_gateway_disconnected(&self, _ecu_names: &[String]) {}
1401 }
1402
1403 /// Builds a minimal `DoipDiagGateway` whose single ECU is backed by the
1404 /// caller-supplied broadcast receiver and mpsc sender.
1405 ///
1406 /// The mpsc `_doip_payload_rx` must be kept alive by the caller for the
1407 /// duration of the test; `send_with_retries` writes into it and will error
1408 /// if the receiver is dropped before it gets a chance to send.
1409 fn make_gateway(
1410 broadcast_rx: broadcast::Receiver<Result<DiagnosticResponse, crate::connections::EcuError>>,
1411 doip_payload_tx: mpsc::Sender<DoipPayload>,
1412 ) -> DoipDiagGateway<TestEcu> {
1413 let ecu = Arc::new(Mutex::new(DoipEcu {
1414 sender: doip_payload_tx,
1415 receiver: broadcast_rx,
1416 }));
1417
1418 let mut ecus = HashMap::new();
1419 ecus.insert(ECU_ADDR, ecu);
1420 let conn = Arc::new(DoipConnection {
1421 ecus,
1422 ip: "127.0.0.1".to_owned(),
1423 });
1424
1425 // DoIPUdpSocket is never touched during send()
1426 let std_sock = UdpSocket::bind("127.0.0.1:0").expect("bind test socket");
1427 std_sock
1428 .set_nonblocking(true)
1429 .expect("set nonblocking for test socket");
1430 let udp_socket = DoIPUdpSocket::new(std_sock, ProtocolVersion::Iso13400_2012)
1431 .expect("create test DoIPUdpSocket");
1432
1433 let mut addr_map: HashMap<u16, usize> = HashMap::new();
1434 addr_map.insert(GATEWAY_ADDR, 0);
1435
1436 let state = DoipGatewayState {
1437 doip_connections: Arc::new(RwLock::new(vec![conn])),
1438 logical_address_to_connection: Arc::new(RwLock::new(addr_map)),
1439 ecus: Arc::new(HashMap::new()),
1440 socket: Arc::new(Mutex::new(Some(udp_socket))),
1441 netmask: 0,
1442 };
1443
1444 DoipDiagGateway {
1445 state,
1446 config: DoipConfig::default(),
1447 variant_detection: VariantDetectionSender::new(mpsc::channel(1).0),
1448 connectivity_handler: Arc::new(TestConnectivityHandler),
1449 lifecycle: Arc::new(GatewayLifecycle::new(TransportState::Enabled)),
1450 }
1451 }
1452
1453 #[tokio::test]
1454 async fn registering_connection_task_does_not_wait_for_connection() {
1455 let tasks = Arc::new(ConnectionTasks::new());
1456
1457 tokio::time::timeout(
1458 Duration::from_millis(100),
1459 spawn_connection_task(&tasks, "pending-connection", std::future::pending()),
1460 )
1461 .await
1462 .expect("task registration must not await connection completion");
1463
1464 assert_eq!(tasks.0.lock().await.len(), 1);
1465 tasks.shutdown().await;
1466 }
1467
1468 /// Constructing a gateway must never create or bind a UDP socket,
1469 /// even though construction succeeds.
1470 /// The socket is created lazily inside `start()`, reached only through an
1471 /// authorized [`DoipDiagGateway::enable`]
[docs]1472 /// [[ test~deferred-doip-passive-construction, Gateway construction does not bind its discovery socket, test ]]
1473 #[tokio::test]
1474 async fn new_never_binds_the_doip_socket() {
1475 let gateway = DoipDiagGateway::<TestEcu>::new(
1476 &DoipConfig::default(),
1477 Arc::new(HashMap::new()),
1478 VariantDetectionSender::new(mpsc::channel(1).0),
1479 Arc::new(TestConnectivityHandler),
1480 )
1481 .await
1482 .unwrap();
1483
1484 assert!(
1485 gateway.state.socket.lock().await.is_none(),
1486 "DoipDiagGateway::new must not bind a socket"
1487 );
1488 assert_eq!(
1489 cda_interfaces::communication_control::TransportControl::state(&gateway).await,
1490 TransportState::Disabled
1491 );
1492 }
1493
1494 fn transmission_params() -> TransmissionParameters {
1495 TransmissionParameters {
1496 gateway_address: GATEWAY_ADDR,
1497 timeout_ack: Duration::from_secs(1),
1498 ecu_name: "test-ecu".to_owned(),
1499 repeat_request_count_transmission: 0,
1500 }
1501 }
1502
1503 fn service_payload() -> ServicePayload {
1504 ServicePayload {
1505 data: REQUEST_DATA.to_vec(),
1506 source_address: TESTER_ADDR,
1507 target_address: ECU_ADDR,
1508 new_session: None,
1509 new_security: None,
1510 }
1511 }
1512
1513 fn ecu_msg_response() -> DiagnosticResponse {
1514 DiagnosticResponse::Msg {
1515 source_address: ECU_ADDR,
1516 target_address: TESTER_ADDR,
1517 data: RESPONSE_DATA.to_vec(),
1518 }
1519 }
1520
1521 fn diag_msg() -> DiagnosticMessage {
1522 DiagnosticMessage {
1523 source_address: TESTER_ADDR.to_be_bytes(),
1524 target_address: ECU_ADDR.to_be_bytes(),
1525 message: REQUEST_DATA.to_vec(),
1526 }
1527 }
1528
1529 type BroadcastTx = broadcast::Sender<Result<DiagnosticResponse, crate::connections::EcuError>>;
1530
1531 fn make_broadcast_pair() -> (
1532 BroadcastTx,
1533 broadcast::Receiver<Result<DiagnosticResponse, crate::connections::EcuError>>,
1534 ) {
1535 broadcast::channel(16)
1536 }
1537
1538 type ResponseChannel = (
1539 mpsc::Sender<Result<Option<TransportResponse>, DiagServiceError>>,
1540 mpsc::Receiver<Result<Option<TransportResponse>, DiagServiceError>>,
1541 );
1542
1543 fn make_response_channel() -> ResponseChannel {
1544 mpsc::channel(1)
1545 }
1546
1547 type SendResult = Result<tokio::task::JoinHandle<()>, cda_interfaces::DiagServiceError>;
1548 type ResponseItem = Result<Option<TransportResponse>, cda_interfaces::DiagServiceError>;
1549
1550 /// Shared test harness.
1551 ///
1552 /// Owns every channel handle that must stay alive for the duration of a
1553 /// test:
1554 /// - `broadcast_tx` - injects simulated ECU responses.
1555 /// - `_doip_payload_rx` - keeps the mpsc sender alive so `send_with_retries`
1556 /// does not error out trying to write the outgoing `DoIP` frame.
1557 /// - `send_handle` - join handle for the background `send()` task.
1558 /// - `response_rx` - receives the UDS responses forwarded by `send()`.
1559 struct TestHarness {
1560 broadcast_tx: broadcast::Sender<Result<DiagnosticResponse, crate::connections::EcuError>>,
1561 _doip_payload_rx: mpsc::Receiver<DoipPayload>,
1562 send_handle: tokio::task::JoinHandle<SendResult>,
1563 response_rx: mpsc::Receiver<ResponseItem>,
1564 }
1565
1566 impl TestHarness {
1567 /// Creates the gateway, spawns the `send()` task, and advances it past
1568 /// `clear_pending_messages` to the point where it is blocked on
1569 /// `ecu.receiver.recv()` inside the `'ack_waiting` loop.
1570 ///
1571 /// Uses `current_thread` flavor (see test attributes) so that
1572 /// `yield_now()` is a reliable synchronization primitive: on a
1573 /// cooperative single-threaded executor each yield hands control to
1574 /// exactly the next scheduled task. The spawned task must pass through
1575 /// the following `.await` points before reaching `recv()`:
1576 ///
1577 /// 1. `ecu_mtx.lock().await` - acquires the ECU mutex
1578 /// 2. `send_with_retries(...).await` - writes the outgoing `DoIP` frame
1579 /// 3. `tokio::time::timeout(...)` - enters the ACK-wait future
1580 /// 4. `ecu.receiver.recv().await` - now blocked, ready for injection
1581 ///
1582 /// Crucially, `clear_pending_messages` runs between steps 1 and 2.
1583 /// We must yield past it before calling `broadcast_tx.send()`, otherwise
1584 /// the injected message would be drained and the task would block forever.
1585 /// One `yield_now()` per await point, plus one safety margin = 5 total.
1586 async fn new() -> Self {
1587 let (broadcast_tx, broadcast_rx) =
1588 broadcast::channel::<Result<DiagnosticResponse, crate::connections::EcuError>>(1);
1589 let (doip_payload_tx, doip_payload_rx) = mpsc::channel::<DoipPayload>(1);
1590
1591 let gateway = make_gateway(broadcast_rx, doip_payload_tx);
1592
1593 let (response_tx, response_rx) = mpsc::channel::<ResponseItem>(1);
1594 let send_handle = tokio::spawn({
1595 let gateway = gateway.clone();
1596 async move {
1597 gateway
1598 .send(transmission_params(), service_payload(), response_tx, true)
1599 .await
1600 }
1601 });
1602
1603 for _ in 0..5 {
1604 tokio::task::yield_now().await;
1605 }
1606
1607 Self {
1608 broadcast_tx,
1609 _doip_payload_rx: doip_payload_rx,
1610 send_handle,
1611 response_rx,
1612 }
1613 }
1614
1615 /// Waits for the next response from the background `send()` task.
1616 ///
1617 /// Uses `tokio::time::timeout` rather than `try_recv()`, so no
1618 /// yield counting is needed after injecting a broadcast message.
1619 async fn recv_response(&mut self) -> ResponseItem {
1620 tokio::time::timeout(Duration::from_millis(500), self.response_rx.recv())
1621 .await
1622 .expect("timed out waiting for response from send() task")
1623 .expect("response channel closed unexpectedly")
1624 }
1625 }
1626
1627 // current_thread: yield_now() in TestHarness::new() must be a reliable sync point.
1628 #[tokio::test(flavor = "current_thread")]
1629 async fn implicit_ack_busy_retry_then_final_response() {
1630 let mut harness = TestHarness::new().await;
1631
1632 // ECU sends BusyRepeatRequest (NRC 0x21) via the implicit-ACK path:
1633 // the DiagnosticMessage arrives before the ACK, so
1634 // wait_for_ack_or_response_until_timeout returns it directly as the
1635 // seed for read_ecu_responses instead of waiting for a separate ACK.
1636 harness
1637 .broadcast_tx
1638 .send(Ok(DiagnosticResponse::Msg {
1639 source_address: ECU_ADDR,
1640 target_address: TESTER_ADDR,
1641 data: vec![
1642 service_ids::NEGATIVE_RESPONSE,
1643 REQUEST_DATA[0],
1644 nrc::BUSY_REPEAT_REQUEST,
1645 ],
1646 }))
1647 .expect("Failed to sent busy repeat request");
1648 let first = harness.recv_response().await;
1649 assert!(
1650 matches!(
1651 first,
1652 Ok(Some(TransportResponse::Pending(
1653 PendingNrc::BusyRepeatRequest { .. }
1654 )))
1655 ),
1656 "expected BusyRepeatRequest, got {first:?}"
1657 );
1658
1659 // ECU sends the final Msg.
1660 harness
1661 .broadcast_tx
1662 .send(Ok(ecu_msg_response()))
1663 .expect("Failed to sent final message");
1664 let second = harness.recv_response().await;
1665 assert!(
1666 matches!(second, Ok(Some(TransportResponse::UdsResponse(_)))),
1667 "expected Message, got {second:?}"
1668 );
1669
1670 drop(harness.response_rx);
1671 tokio::time::timeout(Duration::from_secs(2), harness.send_handle)
1672 .await
1673 .expect("send task did not finish")
1674 .expect("send task panicked")
1675 .expect("send() returned an error");
1676 }
1677
1678 // current_thread: yield_now() in TestHarness::new() must be a reliable sync point.
1679 #[tokio::test(flavor = "current_thread")]
1680 async fn implicit_ack_single_response() {
1681 let mut harness = TestHarness::new().await;
1682
1683 // ECU sends a single Msg - no ACK (implicit ACK path).
1684 harness
1685 .broadcast_tx
1686 .send(Ok(ecu_msg_response()))
1687 .expect("Failed to sent message");
1688 let response = harness.recv_response().await;
1689 assert!(
1690 matches!(response, Ok(Some(TransportResponse::UdsResponse(_)))),
1691 "expected Message, got {response:?}"
1692 );
1693
1694 // No further messages; dropping the receiver unblocks read_ecu_responses.
1695 drop(harness.response_rx);
1696 tokio::time::timeout(Duration::from_secs(2), harness.send_handle)
1697 .await
1698 .expect("send task did not finish")
1699 .expect("send task panicked")
1700 .expect("send() returned an error");
1701 }
1702 #[tokio::test]
1703 async fn wait_for_ack_or_response_ok_none_on_explicit_ack() {
1704 let (tx, mut rx) = make_broadcast_pair();
1705 let msg = diag_msg();
1706 tx.send(Ok(DiagnosticResponse::Ack((ECU_ADDR, vec![]))))
1707 .expect("Failed to send ack");
1708
1709 let result = wait_for_ack_or_response_until_timeout(
1710 &mut rx,
1711 "test-ecu",
1712 &msg,
1713 Duration::from_secs(1),
1714 )
1715 .await;
1716 assert!(
1717 matches!(result, Ok(None)),
1718 "expected Ok(None), got {result:?}"
1719 );
1720 }
1721
1722 /// A `DiagnosticMessage` received before the ACK triggers the implicit-ACK
1723 /// path: the response is returned as `Ok(Some(_))` for the caller to seed
1724 /// into `read_ecu_responses`.
1725 #[tokio::test]
1726 async fn wait_for_ack_or_response_ok_some_on_diagnostic_msg() {
1727 let (tx, mut rx) = make_broadcast_pair();
1728 let msg = diag_msg();
1729 tx.send(Ok(ecu_msg_response()))
1730 .expect("Failed to send message");
1731
1732 let result = wait_for_ack_or_response_until_timeout(
1733 &mut rx,
1734 "test-ecu",
1735 &msg,
1736 Duration::from_secs(1),
1737 )
1738 .await;
1739
1740 assert!(
1741 matches!(result, Ok(Some(DiagnosticResponse::Msg { .. }))),
1742 "expected Ok(Some(Diagnostic)), got {result:?}"
1743 );
1744 }
1745
1746 /// `BusyRepeatRequest` before the ACK also returns `Ok(Some(_))`.
1747 #[tokio::test]
1748 async fn wait_for_ack_or_response_ok_some_on_busy_repeat_request() {
1749 let (tx, mut rx) = make_broadcast_pair();
1750 let msg = diag_msg();
1751
1752 tx.send(Ok(DiagnosticResponse::Msg {
1753 source_address: ECU_ADDR,
1754 target_address: TESTER_ADDR,
1755 data: vec![
1756 service_ids::NEGATIVE_RESPONSE,
1757 REQUEST_DATA[0],
1758 nrc::BUSY_REPEAT_REQUEST,
1759 ],
1760 }))
1761 .expect("Failed to busy repeat request");
1762
1763 let result = wait_for_ack_or_response_until_timeout(
1764 &mut rx,
1765 "test-ecu",
1766 &msg,
1767 Duration::from_secs(1),
1768 )
1769 .await;
1770
1771 assert!(
1772 matches!(result, Ok(Some(DiagnosticResponse::Msg { .. }))),
1773 "expected Ok(Some(Diagnostic(BusyRepeatRequest NRC))), got {result:?}"
1774 );
1775 }
1776
1777 /// A `DiagnosticMessageNack` returns `Err(Nack(_))`.
1778 #[tokio::test]
1779 async fn wait_for_ack_or_response_err_on_nack() {
1780 use doip_definitions::payload::{DiagnosticMessageNack, DiagnosticNackCode};
1781
1782 let (tx, mut rx) = make_broadcast_pair();
1783 let msg = diag_msg();
1784
1785 tx.send(Ok(DiagnosticResponse::Nack(DiagnosticMessageNack {
1786 source_address: ECU_ADDR.to_be_bytes(),
1787 target_address: TESTER_ADDR.to_be_bytes(),
1788 nack_code: DiagnosticNackCode::UnknownTargetAddress,
1789 })))
1790 .expect("Failed to send nack");
1791
1792 let result = wait_for_ack_or_response_until_timeout(
1793 &mut rx,
1794 "test-ecu",
1795 &msg,
1796 Duration::from_secs(1),
1797 )
1798 .await;
1799
1800 assert!(
1801 matches!(result, Err(DiagServiceError::Nack(_))),
1802 "expected Nack, got {result:?}"
1803 );
1804 }
1805
1806 /// A `GenericNack` returns `Err(Nack(_))`.
1807 #[tokio::test]
1808 async fn wait_for_ack_or_response_err_on_generic_nack() {
1809 use doip_definitions::payload::{GenericNack, NackCode};
1810
1811 let (tx, mut rx) = make_broadcast_pair();
1812 let msg = diag_msg();
1813
1814 tx.send(Ok(DiagnosticResponse::GenericNack(GenericNack {
1815 nack_code: NackCode::InvalidPayloadLength,
1816 })))
1817 .expect("Failed to send generic nack");
1818
1819 let result = wait_for_ack_or_response_until_timeout(
1820 &mut rx,
1821 "test-ecu",
1822 &msg,
1823 Duration::from_secs(1),
1824 )
1825 .await;
1826
1827 assert!(
1828 matches!(result, Err(DiagServiceError::Nack(_))),
1829 "expected Nack, got {result:?}"
1830 );
1831 }
1832
1833 /// When the timeout expires with no message, `Err(Timeout)` is returned.
1834 #[tokio::test]
1835 async fn wait_for_ack_or_response_err_on_timeout() {
1836 let (_tx, mut rx) = make_broadcast_pair();
1837 let msg = diag_msg();
1838
1839 let result = wait_for_ack_or_response_until_timeout(
1840 &mut rx,
1841 "test-ecu",
1842 &msg,
1843 Duration::from_millis(200),
1844 )
1845 .await;
1846
1847 assert!(
1848 matches!(result, Err(DiagServiceError::Timeout)),
1849 "expected Timeout, got {result:?}"
1850 );
1851 }
1852
1853 /// When the broadcast receiver is closed, `Err(NoResponse(_))` is returned.
1854 #[tokio::test]
1855 async fn wait_for_ack_or_response_err_when_receiver_closed() {
1856 let (tx, mut rx) = make_broadcast_pair();
1857 let msg = diag_msg();
1858
1859 drop(tx);
1860
1861 let result = wait_for_ack_or_response_until_timeout(
1862 &mut rx,
1863 "test-ecu",
1864 &msg,
1865 Duration::from_secs(1),
1866 )
1867 .await;
1868
1869 assert!(
1870 matches!(result, Err(DiagServiceError::NoResponse(_))),
1871 "expected NoResponse, got {result:?}"
1872 );
1873 }
1874
1875 /// An unrelated message type (e.g. `AliveCheckResponse`) must be skipped;
1876 /// the loop should continue until the real ACK arrives.
1877 #[tokio::test]
1878 async fn wait_for_ack_or_response_skips_unrelated_messages() {
1879 let (tx, mut rx) = make_broadcast_pair();
1880 let msg = diag_msg();
1881
1882 tx.send(Ok(DiagnosticResponse::AliveCheckResponse))
1883 .expect("Failed to send alive check response");
1884 tx.send(Ok(DiagnosticResponse::Ack((ECU_ADDR, vec![]))))
1885 .expect("Failed to send ack");
1886
1887 let result = wait_for_ack_or_response_until_timeout(
1888 &mut rx,
1889 "test-ecu",
1890 &msg,
1891 Duration::from_secs(1),
1892 )
1893 .await;
1894
1895 drop(tx);
1896 assert!(
1897 matches!(result, Ok(None)),
1898 "expected Ok(None), got {result:?}"
1899 );
1900 }
1901
1902 #[tokio::test]
1903 async fn read_ecu_responses_forwards_message_and_exits_on_sender_close() {
1904 let (tx, mut rx) = make_broadcast_pair();
1905 let (resp_tx, mut resp_rx) = make_response_channel();
1906
1907 tx.send(Ok(ecu_msg_response()))
1908 .expect("Failed to send message");
1909
1910 // Spawn the response reader in a separate task.
1911 let handle = tokio::spawn(async move {
1912 read_ecu_responses(&mut rx, "test-ecu", &resp_tx, None).await;
1913 });
1914
1915 // Read the forwarded Msg.
1916 let item = tokio::time::timeout(Duration::from_secs(1), resp_rx.recv())
1917 .await
1918 .expect("timeout")
1919 .expect("channel closed");
1920 assert!(
1921 matches!(item, Ok(Some(TransportResponse::UdsResponse(_)))),
1922 "expected Message, got {item:?}"
1923 );
1924
1925 // Drop the receiver to close the sender; the loop should exit.
1926 drop(resp_rx);
1927 tokio::time::timeout(Duration::from_secs(2), handle)
1928 .await
1929 .expect("read_ecu_responses did not exit after sender closed")
1930 .expect("task panicked");
1931 }
1932
1933 #[tokio::test]
1934 async fn read_ecu_responses_forwards_multiple_responses() {
1935 let (tx, mut rx) = make_broadcast_pair();
1936 let (resp_tx, mut resp_rx) = make_response_channel();
1937 tx.send(Ok(DiagnosticResponse::Msg {
1938 source_address: ECU_ADDR,
1939 target_address: TESTER_ADDR,
1940 data: vec![
1941 service_ids::NEGATIVE_RESPONSE,
1942 REQUEST_DATA[0],
1943 nrc::RESPONSE_PENDING,
1944 ],
1945 }))
1946 .expect("Failed to send pending response");
1947 tx.send(Ok(ecu_msg_response()))
1948 .expect("Failed to send response");
1949 // Keep tx alive across awaits so the channel is not closed while the
1950 // spawned task is reading buffered messages.
1951 let _tx = tx;
1952
1953 let handle = tokio::spawn(async move {
1954 read_ecu_responses(&mut rx, "test-ecu", &resp_tx, None).await;
1955 });
1956
1957 let first = tokio::time::timeout(Duration::from_secs(1), resp_rx.recv())
1958 .await
1959 .expect("timeout")
1960 .expect("closed");
1961 assert!(
1962 matches!(
1963 first,
1964 Ok(Some(TransportResponse::Pending(
1965 PendingNrc::ResponsePending { .. }
1966 )))
1967 ),
1968 "expected ResponsePending, got {first:?}"
1969 );
1970
1971 let second = tokio::time::timeout(Duration::from_secs(1), resp_rx.recv())
1972 .await
1973 .expect("timeout")
1974 .expect("closed");
1975 assert!(
1976 matches!(second, Ok(Some(TransportResponse::UdsResponse(_)))),
1977 "expected Message, got {second:?}"
1978 );
1979
1980 drop(resp_rx);
1981 tokio::time::timeout(Duration::from_secs(2), handle)
1982 .await
1983 .expect("task did not exit")
1984 .expect("task panicked");
1985 }
1986
1987 /// When `first` is `Some`, it is forwarded before reading from the receiver -
1988 /// this is the implicit-ACK seed path.
1989 #[tokio::test]
1990 async fn read_ecu_responses_forwards_first_then_receiver() {
1991 let (tx, mut rx) = make_broadcast_pair();
1992 let (resp_tx, mut resp_rx) = make_response_channel();
1993
1994 let first = ecu_msg_response();
1995 tx.send(Ok(DiagnosticResponse::Msg {
1996 source_address: ECU_ADDR,
1997 target_address: TESTER_ADDR,
1998 data: vec![
1999 service_ids::NEGATIVE_RESPONSE,
2000 REQUEST_DATA[0],
2001 nrc::RESPONSE_PENDING,
2002 ],
2003 }))
2004 .expect("Failed to send pending response");
2005 let _tx = tx;
2006
2007 let handle = tokio::spawn(async move {
2008 read_ecu_responses(&mut rx, "test-ecu", &resp_tx, Some(first)).await;
2009 });
2010
2011 let first_item = tokio::time::timeout(Duration::from_secs(1), resp_rx.recv())
2012 .await
2013 .expect("timeout")
2014 .expect("closed");
2015 assert!(
2016 matches!(first_item, Ok(Some(TransportResponse::UdsResponse(_)))),
2017 "expected Message from first, got {first_item:?}"
2018 );
2019
2020 let second_item = tokio::time::timeout(Duration::from_secs(1), resp_rx.recv())
2021 .await
2022 .expect("timeout")
2023 .expect("closed");
2024 assert!(
2025 matches!(
2026 second_item,
2027 Ok(Some(TransportResponse::Pending(
2028 PendingNrc::ResponsePending { .. }
2029 )))
2030 ),
2031 "expected ResponsePending from receiver, got {second_item:?}"
2032 );
2033
2034 drop(resp_rx);
2035 tokio::time::timeout(Duration::from_secs(2), handle)
2036 .await
2037 .expect("task did not exit")
2038 .expect("task panicked");
2039 }
2040}