Skip to main content

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