Skip to main content

ace_sim/
can_bus.rs

1//! CAN-aware simulation bus
2//!
3//! Models a CAN bus between an ISO-TP node and one or more ECU nodes. CAN is connection-less -no
4//! session state, no handshake. The bus carries raw CAN frames with CAN-specific fault injection
5//! layered on top of the message-level faults from `FaultConfig`.
6//!
7//! CAN-specific faults:
8//!     - Bit error: a transmitted frame is corrupted at the bit level
9//!     - Arbitration loss: a frame is silently dropped as if lost to arbitration (another node won
10//!     the bus)
11//!     - Bus-off: the bus enters an error state and stops delivering frames until reset.
12
13// region: Imports
14
15use crate::{
16    bus::{Envelope, SimBus},
17    clock::{Duration, Instant},
18    fault::FaultConfig,
19    io::NodeAddress,
20    rng::{Rng, Xorshift64},
21};
22use ace_core::Vec;
23
24// endregion: Imports
25
26// region: CanFaultConfig
27
28// CAN-level fault configuration.
29#[derive(Debug, Clone)]
30pub struct CanFaultConfig {
31    /// Underlying message-level fault config.
32    pub message: FaultConfig,
33
34    /// Probability a frame is dropped due to arbitration loss
35    pub arbitration_loss: (u32, u32),
36
37    /// Probability a frame triggers a bit error (corruption + retry drop).
38    pub bit_error: (u32, u32),
39
40    /// Probability the bus enters bus-off state on any given tick. When bus-off, all frames are
41    /// dropped until `reset_bus_off()`.
42    pub bus_off: (u32, u32),
43}
44
45impl CanFaultConfig {
46    pub fn none() -> Self {
47        Self {
48            message: FaultConfig::none(),
49            arbitration_loss: (0, 1),
50            bit_error: (0, 1),
51            bus_off: (0, 1),
52        }
53    }
54
55    pub fn light() -> Self {
56        Self {
57            message: FaultConfig::light(),
58            arbitration_loss: (1, 200),
59            bit_error: (1, 200),
60            bus_off: (1, 200),
61        }
62    }
63
64    pub fn chaos() -> Self {
65        Self {
66            message: FaultConfig::chaos(),
67            arbitration_loss: (1, 10),
68            bit_error: (1, 10),
69            bus_off: (1, 10),
70        }
71    }
72}
73
74// endregion: CanFaultConfig
75
76// region: CanBusState
77
78// The operational state of the simulated CAN bus.
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum CanBusState {
81    /// Bus is operational - frames are delivered normally.
82    Active,
83
84    /// Bus is in error-passive state - frames may still be delivered but error counts are
85    /// elevated.
86    ErrorPassive,
87
88    /// Bus-off - no frames are delivered until the bus is reset.
89    BusOff { since: Instant },
90}
91
92impl CanBusState {
93    pub fn is_operational(&self) -> bool {
94        matches!(self, Self::Active | Self::ErrorPassive)
95    }
96}
97
98// endregion: CanBusState
99
100// region: CanEvent
101
102/// Events the `CanSimBus` delivers to nodes alongside messages.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum CanEvent {
105    /// Bus transitioned to bus-off state.
106    BusOff,
107
108    /// Bus recovered from bus-off state.
109    BusRecovered,
110
111    /// A bit error was detected on a frame from `src`.
112    BitError { src: NodeAddress },
113}
114
115// endregion: CanEvent
116
117// region: CanSimBus
118
119/// A CAN-aware simulation bus.
120///
121/// Wraps `SimBus` for message delivery and adds CAN bus state and CAN-specific fault injection.
122/// Connection-less - any node may send to any other node as long as the bus is operational.
123///
124/// `N` - max frame payload bytes (8 for classic CAN, 64 for CAN FD)
125/// `Q` - max frames in-flight simultaneously
126#[derive(Debug)]
127pub struct CanSimBus<const MAX_DATA: usize, const MAX_QUEUED: usize> {
128    /// Underlying message bus.
129    inner: SimBus<MAX_DATA, MAX_QUEUED>,
130
131    /// CAN fault configuration.
132    can_faults: CanFaultConfig,
133
134    /// Current bus state.
135    bus_state: CanBusState,
136
137    /// Accumulated CAN events for nodes to drain.
138    events: Vec<CanEvent, 16>,
139
140    /// Dedicated RNG for CAN-level fault decisions.
141    rng: Xorshift64,
142}
143
144impl<const MAX_DATA: usize, const MAX_QUEUED: usize> CanSimBus<MAX_DATA, MAX_QUEUED> {
145    /// Creates a new `CanSimBus`.
146    ///
147    /// `seed` - seeds both the message bus RNG and the CAN fault RNG. The CAN RNG uses
148    /// `seed.wrapping_add(2)` for independence.
149    pub fn new(seed: u64, faults: CanFaultConfig) -> Self {
150        Self {
151            inner: SimBus::new(seed, faults.message.clone()),
152            can_faults: faults,
153            bus_state: CanBusState::Active,
154            events: Vec::new(),
155            rng: Xorshift64::new(seed.wrapping_add(2)),
156        }
157    }
158
159    // region: Message Delivery
160
161    /// Enqueues a CAN frame - rejected if the bus is in bus-off state or CAN-level fault injection
162    /// drops it.
163    ///
164    /// Returns `true` if the frame was accepted.
165    pub fn send(&mut self, src: NodeAddress, dst: NodeAddress, data: &[u8]) -> bool {
166        if !self.bus_state.is_operational() {
167            return false;
168        }
169
170        if self.rng.chance(
171            self.can_faults.arbitration_loss.0,
172            self.can_faults.arbitration_loss.1,
173        ) {
174            return false;
175        }
176
177        if self
178            .rng
179            .chance(self.can_faults.bit_error.0, self.can_faults.bit_error.1)
180        {
181            let _ = self.events.push(CanEvent::BitError { src: src.clone() });
182            return false;
183        }
184
185        self.inner.send(src, dst, data)
186    }
187
188    // endregion: Message Delivery
189
190    // region: Bus State Management
191
192    /// Manually triggers bus-off - useful for DST fault injection.
193    pub fn trigger_bus_off(&mut self) {
194        let now = self.inner.now();
195
196        self.bus_state = CanBusState::BusOff { since: now };
197
198        let _ = self.events.push(CanEvent::BusOff);
199    }
200
201    /// Resets the bus from bus-off state back to Active.
202    pub fn reset_bus_off(&mut self) {
203        if matches!(self.bus_state, CanBusState::BusOff { .. }) {
204            self.bus_state = CanBusState::Active;
205            let _ = self.events.push(CanEvent::BusRecovered);
206        }
207    }
208
209    pub fn bus_state(&self) -> &CanBusState {
210        &self.bus_state
211    }
212
213    // endregion: Bus State Management
214
215    // region: Tick
216
217    /// Advances simulation time, delivers due frames, and checks bus-off fault injection.
218    pub fn tick(&mut self, duration: Duration) -> Vec<Envelope<MAX_DATA>, MAX_QUEUED> {
219        if self.bus_state.is_operational() {
220            if self
221                .rng
222                .chance(self.can_faults.bus_off.0, self.can_faults.bus_off.1)
223            {
224                let now = self.inner.now();
225
226                self.bus_state = CanBusState::BusOff { since: now };
227
228                let _ = self.events.push(CanEvent::BusOff);
229            }
230        }
231
232        if !self.bus_state.is_operational() {
233            let _ = self.inner.tick(duration);
234            return Vec::new();
235        }
236
237        self.inner.tick(duration)
238    }
239
240    // endregion: Tick
241
242    // region: Accessors
243
244    pub fn now(&self) -> Instant {
245        self.inner.now()
246    }
247
248    /// Drains accumulated CAN events.
249    pub fn drain_events(&mut self) -> impl Iterator<Item = CanEvent> + '_ {
250        self.events.drain(..)
251    }
252
253    pub fn set_faults(&mut self, faults: CanFaultConfig) {
254        self.inner.set_faults(faults.message.clone());
255        self.can_faults = faults;
256    }
257
258    pub fn inner_mut(&mut self) -> &mut SimBus<MAX_DATA, MAX_QUEUED> {
259        &mut self.inner
260    }
261
262    // endregion: Accessors
263}
264
265// endregion: CanSimBus