Skip to main content

ace_client/
client.rs

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