Skip to main content

acex_server/
config.rs

1// region: Imports
2
3use acex_core::Vec;
4use acex_sim::clock::Duration;
5use acex_uds::message::DiagnosticSessionType;
6// endregion: Imports
7
8// region: Periodic Rate Presets
9
10/// Periodic transmission rate presets.
11/// Arbitrary intervals are supported - these are provided for convenience.
12pub mod periodic {
13    use acex_sim::clock::Duration;
14
15    /// 2000ms - Slow
16    pub const SLOW: Duration = Duration::from_millis(2_000);
17
18    /// 500ms - Medium
19    pub const MEDIUM: Duration = Duration::from_millis(500);
20
21    /// 50ms - Fast
22    pub const FAST: Duration = Duration::from_millis(50);
23}
24
25// endregion: Periodic Rate Presets
26
27// region: Session Config
28
29/// Configuration for a single UDS diagnostic session.
30///
31/// Mirrors the session configuration in an ODX ECU-DESC container.
32#[derive(Debug, Clone)]
33#[cfg_attr(feature = "defmt", derive(defmt::Format))]
34pub struct SessionConfig {
35    /// UDS session type byte.
36    /// 0x01 Default Session, 0x02 Programming Session, 0x03 ExtendedSession.
37    pub session_type: u8,
38
39    /// P2 server - max time to respond before the tester times out (ms)
40    pub p2_timeout: Duration,
41
42    /// P2* server - max time after sending 0x78 Response Pending before the final response must be
43    /// sent (ms)
44    pub p2_extended_timeout: Duration,
45
46    /// S3 server - max time between Tester Present messages before the server drops back to
47    /// Default Session (ms)
48    ///
49    /// If [`Duration::ZERO`] is specified, the session will never transition to
50    /// Default due to a timeout.
51    pub s3_timeout: Duration,
52}
53
54impl SessionConfig {
55    pub const fn default_session() -> Self {
56        Self {
57            session_type: 0x01,
58            p2_timeout: Duration::from_millis(50),
59            p2_extended_timeout: Duration::from_millis(5_000),
60            s3_timeout: Duration::from_millis(5_000),
61        }
62    }
63
64    pub const fn programming_session() -> Self {
65        Self {
66            session_type: 0x02,
67            p2_timeout: Duration::from_millis(50),
68            p2_extended_timeout: Duration::from_millis(5_000),
69            s3_timeout: Duration::from_millis(5_000),
70        }
71    }
72
73    pub const fn extended_session() -> Self {
74        Self {
75            session_type: 0x03,
76            p2_timeout: Duration::from_millis(50),
77            p2_extended_timeout: Duration::from_millis(5_000),
78            s3_timeout: Duration::from_millis(5_000),
79        }
80    }
81}
82
83// endregion: Session Config
84
85// region: Service Config
86
87/// Configuration for a supported UDS service.
88///
89/// Mirrors a DiagService entry in an ODX file.
90#[derive(Debug, Clone)]
91#[cfg_attr(feature = "defmt", derive(defmt::Format))]
92pub struct ServiceConfig {
93    /// UDS service ID byte (e.g 0x22 for ReadDataByIdentifier).
94    pub service_id: u8,
95
96    /// Session types in which this service is available. The server returns 0x7F NRC Service Not
97    /// Supported In Active Session if a request arrives outside these sessions.
98    pub supported_in: &'static [u8],
99
100    /// Minimum security level required. 0 = no security required.
101    pub security_level: u8,
102}
103
104impl ServiceConfig {
105    pub const fn new(service_id: u8, supported_in: &'static [u8]) -> Self {
106        ServiceConfig {
107            service_id,
108            supported_in,
109            security_level: 0,
110        }
111    }
112
113    pub const fn secured(service_id: u8, supported_in: &'static [u8], security_level: u8) -> Self {
114        Self {
115            service_id,
116            supported_in,
117            security_level,
118        }
119    }
120}
121
122// endregion: Service Config
123
124// region: DID Config
125
126/// Configuration for a single Data Identifier (DID).
127///
128/// Mirrors a DataObject entry in an ODX file.
129#[derive(Debug, Clone)]
130#[cfg_attr(feature = "defmt", derive(defmt::Format))]
131pub struct DidConfig {
132    /// 2-byte DID value.
133    pub identifier: u16,
134
135    /// Sessions in which this DID may be read. Empty = not readable.
136    pub readable_in: &'static [u8],
137
138    /// Sessions in which this DID may be written. Empty = not writable.
139    pub writable_in: &'static [u8],
140
141    /// Minimum security level to access this DID. 0 = no security.
142    pub security_level: u8,
143
144    /// Whether this DID may be scheduled for periodic transmission (0x2A).
145    pub periodic: bool,
146
147    /// Minimum interval the server will honor for periodic scheduling. Client-requested intervals
148    /// shorter than this are clamped up.
149    pub min_periodic_interval: Duration,
150}
151
152impl DidConfig {
153    pub const fn read_only(identifier: u16, readable_in: &'static [u8]) -> Self {
154        Self {
155            identifier,
156            readable_in,
157            writable_in: &[],
158            security_level: 0,
159            periodic: false,
160            min_periodic_interval: Duration::from_millis(50),
161        }
162    }
163
164    pub const fn read_write(
165        identifier: u16,
166        readable_in: &'static [u8],
167        writable_in: &'static [u8],
168    ) -> Self {
169        Self {
170            identifier,
171            readable_in,
172            writable_in,
173            security_level: 0,
174            periodic: false,
175            min_periodic_interval: Duration::from_millis(50),
176        }
177    }
178
179    pub const fn periodic(mut self, min_interval: Duration) -> Self {
180        self.periodic = true;
181        self.min_periodic_interval = min_interval;
182        self
183    }
184
185    pub const fn secured(mut self, level: u8) -> Self {
186        self.security_level = level;
187        self
188    }
189}
190
191// endregion: DID Config
192
193// region: Security Level Config
194
195/// Configuration for a single security access level.
196///
197/// Mirrors a Security entry in an ODX file.
198#[derive(Debug, Clone)]
199#[cfg_attr(feature = "defmt", derive(defmt::Format))]
200pub struct SecurityLevelConfig {
201    /// Request Seed byte for this level (always odd: 0x01, 0x03, 0x05 ...).
202    pub level: u8,
203
204    /// Max failed key attempts before lockout is applied.
205    pub max_attempts: Option<u8>,
206
207    /// Duration of the lockout after exceeding max attempts.
208    pub lockout_duration: Option<Duration>,
209
210    /// Expected seed length in bytes.
211    pub seed_length: usize,
212
213    /// Expected key length in bytes.
214    pub key_length: usize,
215}
216
217// endregion: Security Level Config
218
219// region: Server Config
220
221/// Complete server configuration - mirrors what an ODX ECU description provides.
222///
223/// Constructed once (typically as a static or const) and referenced by the server state machine.
224/// All look-ups are O(n) over the small, fixed-size `heapless::Vec` collections - appropriate for
225/// the sizes involved.
226#[derive(Debug, Clone)]
227#[cfg_attr(all(feature = "defmt", not(feature = "alloc")), derive(defmt::Format))]
228pub struct ServerConfig<
229    const MAX_SESSIONS: usize,
230    const MAX_SERVICES: usize,
231    const MAX_DIDS: usize,
232    const MAX_SECURITY_LEVELS: usize,
233> {
234    /// Physical address this server responds to.
235    pub physical_address: u16,
236
237    /// Functional (broadcast) address this server listens on.
238    pub functional_address: u16,
239
240    pub sessions: Vec<SessionConfig, MAX_SESSIONS>,
241    pub services: Vec<ServiceConfig, MAX_SERVICES>,
242    pub data_identifiers: Vec<DidConfig, MAX_DIDS>,
243    pub security_levels: Vec<SecurityLevelConfig, MAX_SECURITY_LEVELS>,
244
245    /// Diagnostic session to enter when the server is initialized or reset.
246    pub default_session_type: DiagnosticSessionType,
247
248    /// Whether reading multiple DIDs at once is allowed.
249    pub allow_read_many_dids: bool
250}
251
252impl<
253        const MAX_SESSIONS: usize,
254        const MAX_SERVICES: usize,
255        const MAX_DIDS: usize,
256        const MAX_SECURITY_LEVELS: usize,
257    > ServerConfig<MAX_SESSIONS, MAX_SERVICES, MAX_DIDS, MAX_SECURITY_LEVELS>
258{
259    pub fn new(physical_address: u16, functional_address: u16) -> Self {
260        Self {
261            physical_address,
262            functional_address,
263            sessions: Vec::new(),
264            services: Vec::new(),
265            data_identifiers: Vec::new(),
266            security_levels: Vec::new(),
267            default_session_type: DiagnosticSessionType::DefaultSession,
268            allow_read_many_dids: false
269        }
270    }
271
272    // region: Builder methods
273
274    pub fn with_session(mut self, s: SessionConfig) -> Self {
275        #[cfg(feature = "defmt")]
276        defmt::unwrap!(self.sessions.push(s));
277
278        #[cfg(not(feature = "defmt"))]
279        let _ = self.sessions.push(s);
280
281        self
282    }
283
284    pub fn with_service(mut self, s: ServiceConfig) -> Self {
285        #[cfg(feature = "defmt")]
286        defmt::unwrap!(self.services.push(s));
287
288        #[cfg(not(feature = "defmt"))]
289        let _ = self.services.push(s);
290
291        self
292    }
293
294    pub fn with_did(mut self, d: DidConfig) -> Self {
295        #[cfg(feature = "defmt")]
296        defmt::unwrap!(self.data_identifiers.push(d));
297
298        #[cfg(not(feature = "defmt"))]
299        let _ = self.data_identifiers.push(d);
300
301        self
302    }
303
304    pub fn with_security_level(mut self, l: SecurityLevelConfig) -> Self {
305        #[cfg(feature = "defmt")]
306        defmt::unwrap!(self.security_levels.push(l));
307
308        #[cfg(not(feature = "defmt"))]
309        let _ = self.security_levels.push(l);
310
311        self
312    }
313
314    pub fn with_default_session_type(mut self, session_type: DiagnosticSessionType) -> Self {
315        self.default_session_type = session_type;
316        self
317    }
318
319    pub fn with_allow_read_many_dids(mut self, status: bool) -> Self {
320        self.allow_read_many_dids = status;
321        self
322    }
323
324    // endregion: Builder methods
325
326    // region: Lookup helpers
327
328    pub fn find_session(&self, session_type: u8) -> Option<&SessionConfig> {
329        self.sessions
330            .iter()
331            .find(|s| s.session_type == session_type)
332    }
333
334    pub fn find_service(&self, service_id: u8) -> Option<&ServiceConfig> {
335        self.services.iter().find(|s| s.service_id == service_id)
336    }
337
338    pub fn find_did(&self, identifier: u16) -> Option<&DidConfig> {
339        self.data_identifiers
340            .iter()
341            .find(|d| d.identifier == identifier)
342    }
343
344    pub fn find_security_level(&self, level: u8) -> Option<&SecurityLevelConfig> {
345        self.security_levels.iter().find(|l| l.level == level)
346    }
347
348    pub fn service_allowed(&self, service_id: u8, session_type: u8) -> bool {
349        self.find_service(service_id)
350            .map(|s| s.supported_in.contains(&session_type))
351            .unwrap_or(false)
352    }
353
354    pub fn service_unlocked(&self, service_id: u8, security_level: u8) -> bool {
355        self.find_service(service_id)
356            .map(|s| security_level >= s.security_level)
357            .unwrap_or(false)
358    }
359
360    pub fn did_readable(&self, identifier: u16, session_type: u8) -> bool {
361        self.find_did(identifier)
362            .map(|s| s.readable_in.contains(&session_type))
363            .unwrap_or(false)
364    }
365
366    pub fn did_writable(&self, identifier: u16, session_type: u8) -> bool {
367        self.find_did(identifier)
368            .map(|s| s.writable_in.contains(&session_type))
369            .unwrap_or(false)
370    }
371
372    // endregion: Lookup helpers
373}
374
375// endregion: Server Config