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