Skip to main content

ace_client/
client.rs

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