1/*
2 * SPDX-FileCopyrightText: 2026 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
14//! Shared contracts for managed diagnostic communication lifecycle control.
15//!
[docs] 16//! [[ dimpl~communication-control-contracts, Communication lifecycle contracts, dimpl ]]
17pub mod access;
18pub mod error;
19pub mod operation;
20// Private: `SwappableGateway` is minted only via `ComponentSlot::transport_control`.
21mod swappable_gateway;
22
23use std::time::Duration;
24
25pub mod disable;
26
27pub use access::{CommunicationAccess, CommunicationError, CommunicationGuard, CommunicationState};
28use async_trait::async_trait;
29pub use disable::{DisableCommunication, DisableError, DisableGuard, DisableReason};
30pub use error::CommControlError;
31pub use operation::{
32 ActivationCause, CommunicationOperation, CommunicationOperationFailure, DetectionCause,
33};
34use serde::{Deserialize, Serialize};
35use tokio::sync::{Mutex, RwLock};
36
37/// Physical diagnostic transport state.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum TransportState {
40 /// Communication is disabled (connections torn down, no traffic).
41 Disabled,
42 /// Communication is being enabled (gateway sequence in progress).
43 Enabling,
44 /// Communication is active and operational.
45 Enabled,
46 /// Communication is being disabled (connections being torn down).
47 Disabling,
48 /// Communication initialization or runtime failed.
49 Failed,
50}
51
52/// Tracks the transport status.
53///
54/// # Ownership model
55///
56/// Each layer owns its own, unshared tracker: the diagnostic transport router
57/// one, and each gateway (`DoipDiagGateway`, `CanDiagGateway`) one. When the
58/// router calls `gateway.enable()`, both transition their own tracker. External
59/// consumers only ever observe the router's, via the transport-control view
60/// minted from `ComponentSlot::transport_control`.
61///
62/// A gateway's own tracker serves its `enable()` idempotency short-circuit, and
63/// is the only source of truth when the gateway is used without a router (tests,
64/// shutdown).
65///
66/// This type only tracks state. Serializing a complete lifecycle operation is
67/// the job of the operation mutex each layer already owns.
68pub struct TransportStateTracker {
69 state: RwLock<TransportState>,
70}
71
72impl TransportStateTracker {
73 /// Creates a tracker initialized to `state`.
74 #[must_use]
75 pub fn new(state: TransportState) -> Self {
76 Self {
77 state: RwLock::new(state),
78 }
79 }
80
81 /// Updates the communication state.
82 pub async fn transition(&self, state: TransportState) {
83 *self.state.write().await = state;
84 }
85
86 /// Returns the current communication state.
87 pub async fn state(&self) -> TransportState {
88 *self.state.read().await
89 }
90
91 /// Returns whether communication is enabled.
92 pub async fn active(&self) -> bool {
93 self.state().await == TransportState::Enabled
94 }
95}
96
97/// Restartable-transport lifecycle scaffolding shared by gateway
98/// implementations (`DoipDiagGateway`, `CanDiagGateway`): one mutex serializing
99/// `enable`/`disable` transitions and holding the transport-specific
100/// background-task state `Operation`, alongside a [`TransportStateTracker`].
101pub struct GatewayLifecycle<Operation> {
102 /// Serializes lifecycle transitions and protects `Operation`'s background-task handles.
103 pub operation: Mutex<Operation>,
104 /// Transport-state tracker. See [`TransportStateTracker`].
105 pub coordinator: TransportStateTracker,
106}
107
108impl<Operation: Default> GatewayLifecycle<Operation> {
109 /// Creates lifecycle scaffolding starting at `initial`, with a
110 /// default-initialized (empty) `Operation`.
111 #[must_use]
112 pub fn new(initial: TransportState) -> Self {
113 Self {
114 operation: Mutex::new(Operation::default()),
115 coordinator: TransportStateTracker::new(initial),
116 }
117 }
118}
119
120/// Controls when diagnostic transport network activation begins.
121#[derive(
122 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
123)]
124pub enum CommunicationInitMode {
125 /// Whole-vehicle communication is initialized immediately during startup.
126 #[default]
127 Always,
128 /// Communication stays uninitialized at startup. HTTP/SOVD start first, and
129 /// the first qualifying ECU diagnostic request, or an explicit
130 /// `CommunicationPlugin::activate()` call, triggers one whole-vehicle
131 /// initialization.
132 OnDemand,
133 /// Communication stays uninitialized at startup and ordinary activation
134 /// (startup, plugin `activate()`, ECU diagnostic requests) is rejected
135 /// without any vehicle-network activity.
136 ///
137 /// Plugin policy, not a framework invariant. The default plugin implements
138 /// this mode by rejecting `activate()` and `request_activate()`. A custom
139 /// plugin may choose a different policy.
140 Disabled,
141}
142
143/// Controls whether whole-vehicle variant detection runs *automatically* when
144/// communication is enabled.
145///
146/// Independent of [`CommunicationInitMode`], which decides *when* communication
147/// comes up. Gates only the registered [`CommunicationVariantDetection`]
148/// implementation, never the transport or the [`CommunicationLifecycle`] hooks.
149///
150/// Neither value disables detection outright. An explicit
151/// `CommunicationPlugin::trigger_detection()` runs the detector in both.
152#[derive(
153 Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema,
154)]
155pub enum VariantDetectionMode {
156 /// The registered detector runs as the last stage of every activation.
157 #[default]
158 Always,
159 /// Activation brings the transport and the lifecycle hooks up only.
160 ///
161 /// Nothing settles [`VariantState`](crate::ecumanager::VariantState) on its
162 /// own, so variant-dependent surfaces (resource listings under an ECU,
163 /// request/response schemas) report not-ready until detection is triggered
164 /// explicitly. Intended for deployments whose communication plugin owns the
165 /// detection decision itself.
166 Never,
167}
168
169/// Controls behavior of diagnostic transport after a runtime database update.
170///
171/// An update always takes the exclusive disable lease, so communication is
172/// unavailable for its duration. This decides only what happens to the lease
173/// once the update finishes.
174#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
175pub enum PostUpdateCommunicationMode {
176 /// After update, bring communication back up. Resumes the transport the
177 /// update displaced, and requests activation if it started from a deferred
178 /// runtime. That request is subject to `init_mode`, so under `Disabled`
179 /// communication stays down regardless.
180 #[default]
181 Enabled,
182 /// After update, leave communication deferred, even if the update displaced
183 /// an enabled runtime. The transport stays down until triggered.
184 Deferred,
185}
186
187/// Default retry hint for requests deferred while communication starts.
188pub const DEFAULT_DEFERRED_RETRY_AFTER: Duration = Duration::from_secs(30);
189
190/// Transport initialization and runtime settings.
191#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema)]
192pub struct CommunicationSettings {
193 /// Controls when diagnostic transport network activation begins:
194 /// - "Always": whole-vehicle communication initializes eagerly at startup (default).
195 /// - "OnDemand": HTTP/SOVD starts first. The first qualifying ECU diagnostic
196 /// request, or an explicit `activate()` call, triggers initialization.
197 /// - "Disabled": HTTP/SOVD starts first. The default communication plugin
198 /// provides no activation path, and `trigger_detection()` needs an
199 /// already-enabled transport.
200 pub init_mode: CommunicationInitMode,
201 /// Controls whether enabling communication also runs whole-vehicle variant
202 /// detection:
203 /// - "Always": the registered detector runs as the last stage of every
204 /// activation (default).
205 /// - "Never": activation brings the transport and its lifecycle hooks up
206 /// only, so variant-dependent surfaces report not-ready until detection is
207 /// triggered explicitly.
208 pub variant_detection: VariantDetectionMode,
209 /// Controls transport behavior after a runtime database update:
210 /// - "Enabled": resume the transport the update displaced, requesting
211 /// activation if it started from a deferred runtime, subject to `init_mode`
212 /// (default).
213 /// - "Deferred": leave communication deferred even if the update displaced
214 /// an enabled runtime. The transport stays down until triggered.
215 pub post_update_mode: PostUpdateCommunicationMode,
216 /// The value (in seconds) for the HTTP `Retry-After` header returned when a
217 /// diagnostic request arrives while initialization is still pending.
218 pub deferred_retry_after_seconds: u64,
219}
220
221impl Default for CommunicationSettings {
222 fn default() -> Self {
223 Self {
224 init_mode: CommunicationInitMode::default(),
225 variant_detection: VariantDetectionMode::default(),
226 post_update_mode: PostUpdateCommunicationMode::default(),
227 deferred_retry_after_seconds: DEFAULT_DEFERRED_RETRY_AFTER.as_secs(),
228 }
229 }
230}
231
232/// Trait for controlling the diagnostic transport lifecycle.
233///
234/// # Layered implementations
235///
236/// The intended call chain is:
237///
238/// ```text
239/// CommunicationHandle -> SwappableGateway -> DiagnosticTransportRouter -> Gateways
240/// ```
241///
242/// The coordinator calls `state()` on the outermost implementor to populate
243/// operation results. It never reaches through to query individual gateway state.
244#[async_trait]
245pub trait TransportControl: Send + Sync + 'static {
246 /// Start the diagnostic transport sequence.
247 /// This must be idempotent.
248 ///
249 /// Implementations must prevent concurrent `enable` and `disable` calls
250 /// from racing. For example, they may serialize the complete operations
251 /// behind a single mutex, or `disable` may cancel an in-flight `enable`
252 /// before tearing down any resources it created.
253 async fn enable(&self) -> Result<(), CommControlError>;
254
255 /// Tear down all connections and disable transport.
256 /// This must be idempotent.
257 ///
258 /// Implementations must prevent this operation from racing with
259 /// [`enable`](Self::enable). See that method for one possible strategy.
260 async fn disable(&self) -> Result<(), CommControlError>;
261
262 /// Returns the current communication state of **this** layer's tracker.
263 ///
264 /// When called on the router, this reflects the router-level lifecycle state.
265 /// Individual gateway states are not visible through this method.
266 async fn state(&self) -> TransportState;
267}
268
269/// Application-level hook into the communication transport lifecycle.
270///
271/// Registered implementations run in registration order on every transport
272/// enable and in reverse order on every transport disable.
273///
274/// [`initialize`](Self::initialize) and [`deinitialize`](Self::deinitialize) are
275/// a matched pair. The framework never calls `initialize` twice without a
276/// `deinitialize` in between, so implementations need not be re-entrant.
277#[async_trait]
278pub trait CommunicationLifecycle: Send + Sync + 'static {
279 /// Stable name used in diagnostics.
280 fn name(&self) -> &str;
281
282 /// Called after the transport is successfully enabled.
283 ///
284 /// The lifecycle state is still `Enabling` at this point: whole-vehicle
285 /// variant detection, and every other hook's `initialize`, may still run
286 /// after this returns, so [`CommunicationAccess::acquire`] fails here.
287 /// Work that needs the state to actually be `Enabled` belongs in
288 /// [`on_enabled`](Self::on_enabled) instead.
289 async fn initialize(&self) -> Result<(), CommControlError>;
290
291 /// Called once the lifecycle state has been published as `Enabled`, after
292 /// every hook's [`initialize`](Self::initialize) and any variant
293 /// detection have completed successfully.
294 ///
295 /// Runs in registration order, on the lifecycle worker, so it must not
296 /// fail and should finish quickly: it delays every later lifecycle
297 /// command, including a disable requested while it runs. The framework
298 /// guarantees it completes before the next matching
299 /// [`deinitialize`](Self::deinitialize), so work started here safely
300 /// assumes communication stays enabled until then.
301 ///
302 /// Not called when a hook's `initialize`, or variant detection, fails; nor
303 /// for an explicit re-detection, which never deinitializes hooks in the
304 /// first place.
305 async fn on_enabled(&self);
306
307 /// Called before the transport is disabled.
308 ///
309 /// Implementations should stop any work that requires an active transport
310 /// (e.g. tester-present keep-alive tasks). Restarting that work once
311 /// communication is enabled again belongs in
312 /// [`on_enabled`](Self::on_enabled), not in
313 /// [`initialize`](Self::initialize), which runs too early for it.
314 ///
315 /// Defaults to a no-op.
316 async fn deinitialize(&self) {}
317}
318
319/// Whole-vehicle variant detection: the last stage of an activation, and the
320/// only thing an explicit detection request runs.
321///
322/// Separate from [`CommunicationLifecycle`], because the two have different
323/// lifetimes. A hook runs on every enable and has a matching teardown, while
324/// detection is optional (see [`VariantDetectionMode`]), repeatable, and has no
325/// teardown counterpart. That lets an enable skip detection, and a detection run
326/// without re-entering any hook's
327/// [`initialize`](CommunicationLifecycle::initialize).
328#[async_trait]
329pub trait CommunicationVariantDetection: Send + Sync + 'static {
330 /// Stable name used in diagnostics.
331 fn name(&self) -> &str;
332
333 /// Runs whole-vehicle variant detection against the live transport.
334 ///
335 /// Called after every registered [`CommunicationLifecycle`] hook has
336 /// initialized, so implementations may rely on the transport and on hook
337 /// state being up.
338 ///
339 /// # Errors
340 ///
341 /// Returns an error when detection cannot be started or completed. The
342 /// framework treats this as an activation failure: hooks are deinitialized
343 /// and the transport is taken back down.
344 async fn detect(&self) -> Result<(), CommControlError>;
345}