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}