ace_client/client.rs
1// region: Imports
2
3use ace_core::Vec;
4use ace_proto::{common::RawFrame, UdsFrame};
5use ace_sim::{clock::Instant, io::NodeAddress};
6use ace_uds::ext::UdsFrameExt;
7
8use crate::{config::ClientConfig, event::ClientEvent, pending::PendingRequest, ClientError};
9
10// endregion: Imports
11
12// region: UDS Client
13
14/// Stateful UDS tester client state machine.
15///
16/// Sends raw UDS request frames and emits [`ClientEvent`]s as responses arrive. The client tracks
17/// only what is necessary for the protocol exchange - P2 and P2* timeouts on pending requests.
18/// Session state, security state, and retry logic are entirely the caller's responsibility.
19///
20/// `N` - maximum number of concurrent pending requests. Defaults to 1. UDS is strictly sequential
21/// in most implementations - use `UdsClient<1>` unless you have a specific need for pipelining.
22#[derive(Debug)]
23pub struct UdsClient<
24 const PENDING: usize,
25 const SIM_MAX_FRAME: usize,
26 const SIM_MAX_OUTBOX: usize,
27 const MAX_TARGET_EVENTS: usize,
28 const PERIODIC_DIDS: usize,
29 const MAX_DATA: usize,
30> {
31 config: ClientConfig,
32 address: NodeAddress,
33 server: NodeAddress,
34 pending: Vec<PendingRequest, PENDING>,
35 outbox: Vec<(NodeAddress, Vec<u8, SIM_MAX_FRAME>), SIM_MAX_OUTBOX>,
36 events: Vec<ClientEvent<MAX_DATA>, MAX_TARGET_EVENTS>,
37 periodic_dids: Vec<u8, PERIODIC_DIDS>,
38}
39
40impl<
41 const PENDING: usize,
42 const SIM_MAX_FRAME: usize,
43 const SIM_MAX_OUTBOX: usize,
44 const MAX_EVENTS: usize,
45 const PERIODIC_DIDS: usize,
46 const MAX_DATA: usize,
47 > UdsClient<PENDING, SIM_MAX_FRAME, SIM_MAX_OUTBOX, MAX_EVENTS, PERIODIC_DIDS, MAX_DATA>
48{
49 pub fn new(config: ClientConfig, address: NodeAddress) -> Self {
50 let server = NodeAddress(config.target_address as u32);
51 Self {
52 config,
53 address,
54 server,
55 pending: Vec::new(),
56 outbox: Vec::new(),
57 events: Vec::new(),
58 periodic_dids: Vec::new(),
59 }
60 }
61
62 // region: SimNode Surface
63
64 pub fn address(&self) -> &NodeAddress {
65 &self.address
66 }
67
68 /// Delivers an inbound frame to the client.
69 ///
70 /// Classifies the frame before attempting any pending request match:
71 ///
72 /// 1. Empty frame - ignored
73 /// 2. `0x7F` - negative response - matched by requested SID at byte 1
74 /// 3. First byte is a subscribed periodic DID - `PeriodicData` event
75 /// 4. First byte has bit 6 set - positive response - matched by request SID
76 /// 5. Anything else - `Unsolicited` event
77 ///
78 /// Periodic classification must precede positive response classification because a periodic
79 /// DID low byte could theoretically have bit 6 set. Explicit subscription registry takes
80 /// priority.
81 pub fn handle(
82 &mut self,
83 _src: &NodeAddress,
84 data: &[u8],
85 now: Instant,
86 ) -> Result<(), ClientError> {
87 // Nothing to do for empty frames
88 let first = match data.first().copied() {
89 Some(b) => b,
90 None => return Ok(()),
91 };
92
93 let frame = UdsFrame::from_slice(data);
94
95 // region: Negative response [0x7F, requested_sid, nrc]
96
97 if first == 0x7F {
98 let requested_sid = data.get(1).copied().unwrap_or(0);
99 let nrc = data.get(2).copied().unwrap_or(0);
100
101 // 0x78 Response Pending - extend deadline, keep pending, emit event
102 if nrc == 0x78 {
103 if let Some(p) = self.pending.iter_mut().find(|p| p.sid == requested_sid) {
104 p.extend(now, self.config.p2_extended_timeout);
105
106 let _ = self
107 .events
108 .push(ClientEvent::ResponsePending { sid: requested_sid });
109 }
110
111 return Ok(());
112 }
113
114 self.complete_pending(requested_sid);
115
116 let _ = self.events.push(ClientEvent::NegativeResponse {
117 sid: requested_sid,
118 nrc,
119 });
120
121 return Ok(());
122 }
123
124 // endregion: Negative response [0x7F, requested_sid, nrc]
125
126 // region: Periodic Data
127 //
128 // [periodic_data_identifier (1 byte), data_record (n bytes)]
129 // No SID prefix - first byte IS the periodic DID low byte. Only classifiable if the client
130 // has a matching subscription.
131
132 if self.periodic_dids.contains(&first) {
133 // payload() gives bytes after the first byte - the data record
134 let raw = frame.as_bytes();
135 let record = raw.get(1..).unwrap_or(&[]);
136 let mut buf: Vec<u8, MAX_DATA> = Vec::new();
137 let _ = buf.extend_from_slice(&record[..record.len().min(MAX_DATA)]);
138
139 let _ = self.events.push(ClientEvent::PeriodicData {
140 did: first,
141 data: buf,
142 });
143
144 return Ok(());
145 }
146
147 // endregion: Periodic Data
148
149 // region: Positive Response - first byte has bit 6 set
150 //
151 // UDS positive response SID = request SID | 0x40.
152 // 0x7F is already handled above so cannot reach here.
153 if first & 0x40 != 0 {
154 let request_sid = first & !0x40u8;
155
156 if self.complete_pending(request_sid) {
157 let payload = frame.payload();
158 let mut buf: Vec<u8, MAX_DATA> = Vec::new();
159 let _ = buf.extend_from_slice(&payload[..payload.len().min(MAX_DATA)]);
160
161 let _ = self.events.push(ClientEvent::PositiveResponse {
162 sid: request_sid,
163 data: buf,
164 });
165
166 return Ok(());
167 }
168 }
169
170 // endregion: Positive Response
171
172 // region: Unsolicited
173 //
174 // Frame did no match any classification or had no matching pending request. Emit raw
175 // bytes for observability.
176 {
177 let raw = frame.as_bytes();
178 let mut buf: Vec<u8, MAX_DATA> = Vec::new();
179 let _ = buf.extend_from_slice(&raw[..raw.len().min(MAX_DATA)]);
180
181 let _ = self.events.push(ClientEvent::Unsolicited { data: buf });
182 }
183
184 // endregion: Unsolicited
185
186 Ok(())
187 }
188
189 /// Advances internal timers - expires timed-out pending requests.
190 pub fn tick(&mut self, now: Instant) -> Result<(), ClientError> {
191 let mut expired: Vec<u8, PENDING> = Vec::new();
192
193 for p in self.pending.iter() {
194 if p.is_expired(now) {
195 let _ = expired.push(p.sid);
196 }
197 }
198
199 for sid in expired {
200 self.complete_pending(sid);
201 let _ = self.events.push(ClientEvent::Timeout { sid });
202 }
203
204 Ok(())
205 }
206
207 /// Drains pending outbound frames into `out`.
208 pub fn drain_outbox(
209 &mut self,
210 out: &mut Vec<(NodeAddress, Vec<u8, SIM_MAX_FRAME>), SIM_MAX_OUTBOX>,
211 ) -> usize {
212 let n = self.outbox.len();
213
214 for item in self.outbox.drain(..) {
215 let _ = out.push(item);
216 }
217
218 n
219 }
220
221 // endregion: SimNode Surface
222
223 // region: Request API
224
225 /// Enqueues a raw UDS request for transmission.
226 ///
227 /// `data` must begin with the SID byte followed by any parameters. The client tracks this as a
228 /// pending request and starts the P2 timer.
229 ///
230 /// Returns `ClientError::QueueFull` if N concurrent requests are already pending.
231 pub fn request(&mut self, data: &[u8], now: Instant) -> Result<(), ClientError> {
232 let sid = match data.first().copied() {
233 Some(b) => b,
234 None => return Err(ClientError::EmptyRequest),
235 };
236
237 if self.pending.len() >= PENDING {
238 return Err(ClientError::QueueFull);
239 }
240
241 let mut frame: Vec<u8, SIM_MAX_FRAME> = Vec::new();
242 let _ = frame.extend_from_slice(data);
243
244 if self.outbox.len() >= SIM_MAX_OUTBOX {
245 return Err(ClientError::OutboxFull);
246 }
247 let _ = self.outbox.push((self.server.clone(), frame));
248
249 let _ = self
250 .pending
251 .push(PendingRequest::new(sid, now, self.config.p2_timeout));
252
253 Ok(())
254 }
255
256 // endregion: Request API
257
258 // region: Periodic Subscription API
259
260 /// Registers the low byte of a periodic DID identifier as subscribed.
261 ///
262 /// Inbound frames whose first byte matches a subscribed DID low byte are classified as
263 /// `PeriodicData` events rather than `Unsolicited`.
264 ///
265 /// The low byte corresponds to the `periodic_data_identifier` field in
266 /// `ReadDataByPeriodicIdentifierResponseData`. For a DID of `0xF201` the low byte is `0x01`.
267 ///
268 /// Silently does nothing if already subscribed or the registry is full.
269 pub fn subscribe_periodic(&mut self, did_low_byte: u8) {
270 if !self.periodic_dids.contains(&did_low_byte) {
271 let _ = self.periodic_dids.push(did_low_byte);
272 }
273 }
274
275 /// Removes a periodic DID subscription.
276 ///
277 /// After calling this, frames with this DID low byte will be classified as `Unsolicited`
278 /// rather than `PeriodicData`.
279 pub fn unsubscribe_periodic(&mut self, did_low_byte: u8) {
280 self.periodic_dids.retain(|&d| d != did_low_byte);
281 }
282
283 /// Returns true if the given DID low byte is currently subscribed.
284 pub fn is_periodic_subscribed(&self, did_low_byte: u8) -> bool {
285 self.periodic_dids.contains(&did_low_byte)
286 }
287
288 // endregion: Periodic Subscription API
289
290 // region: Event API
291
292 /// Drains all accumulated events, returning an iterator.
293 ///
294 /// Events are consumed - calling `drain_events` twice returns events on the first call and
295 /// nothing on the second.
296 pub fn drain_events(&mut self) -> impl Iterator<Item = ClientEvent<MAX_DATA>> + '_ {
297 self.events.drain(..)
298 }
299
300 /// Returns true if any events are pending.
301 pub fn has_events(&self) -> bool {
302 !self.events.is_empty()
303 }
304
305 /// Returns the number of currently pending requests.
306 pub fn pending_count(&self) -> usize {
307 self.pending.len()
308 }
309
310 // endregion: Event API
311
312 // region: Internal Helpers
313
314 /// Removes a pending request by SID. Returns true if found and removed.
315 fn complete_pending(&mut self, sid: u8) -> bool {
316 if let Some(pos) = self.pending.iter().position(|p| p.sid == sid) {
317 self.pending.remove(pos);
318 true
319 } else {
320 false
321 }
322 }
323
324 // endregion: Internal Helpers
325}
326
327// endregion: UDS Client