Skip to main content

zerodds_xrce_client/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! XRCE Client — synchrones Interface ohne Callbacks (Spec §7.2).
5//!
6//! Crate `zerodds-xrce-client`.
7//!
8//! # Spec-Mapping
9//!
10//! OMG DDS-XRCE 1.0 §7.2: "XRCE Client: simplified Interface, keine
11//! Callbacks, Text-Parameter; Session ueberbrueckt Sleep/Wakeup-
12//! Zyklen."
13//!
14//! Wir liefern eine synchrone State-Machine [`XrceClient`] mit
15//! folgenden Operationen:
16//!
17//! | Methode             | Spec-Bezug              |
18//! |---------------------|-------------------------|
19//! | [`XrceClient::new`] | §7.8.2 (CREATE_CLIENT)  |
20//! | [`XrceClient::connect`]    | §8.4.5 (Handshake) |
21//! | [`XrceClient::create_object`] | §7.8.3 (CREATE) |
22//! | [`XrceClient::delete_object`] | §7.8.3 (DELETE) |
23//! | [`XrceClient::request_write`] | §7.8.4 (WRITE_DATA) |
24//! | [`XrceClient::request_read`]  | §7.8.5 (READ_DATA) |
25//! | [`XrceClient::disconnect`]    | §8.4.5 |
26//!
27//! Transport ist abstrahiert via [`ClientTransport`]-Trait — konkrete
28//! Impls (UDP/TCP/DTLS/Serial) leben in `crates/xrce/src/transport_*`.
29//!
30//! Safety classification: **SAFE**.
31
32#![cfg_attr(not(feature = "std"), no_std)]
33#![forbid(unsafe_code)]
34#![warn(missing_docs)]
35
36extern crate alloc;
37
38use alloc::vec::Vec;
39
40use zerodds_xrce::header::ClientKey;
41use zerodds_xrce::object_id::ObjectId;
42use zerodds_xrce::object_repr::ObjectVariant;
43use zerodds_xrce::submessages::Submessage;
44
45/// Transport-abstraktion fuer den XRCE-Client.
46///
47/// Implementierungen (UDP/TCP/DTLS/Serial) liegen in
48/// `crates/xrce/src/transport_*.rs`.
49pub trait ClientTransport {
50    /// Sendet ein Submessage-Bundle an den Agent.
51    ///
52    /// # Errors
53    /// Transport-spezifisch.
54    fn send(&mut self, payload: &[u8]) -> Result<(), ClientError>;
55
56    /// Pollt eingehende Daten vom Agent. Blockiert nicht — wenn keine
57    /// Daten anliegen, liefert `Ok(None)`.
58    ///
59    /// # Errors
60    /// Transport-spezifisch.
61    fn try_recv(&mut self) -> Result<Option<Vec<u8>>, ClientError>;
62}
63
64/// XRCE-Client-Lifecycle-Status.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum ClientState {
67    /// Initial: Client kennt seine ClientKey, ist aber nicht
68    /// verbunden.
69    Disconnected,
70    /// Connect-Handshake im Gange (CREATE_CLIENT gesendet, STATUS_AGENT
71    /// noch nicht empfangen).
72    Connecting,
73    /// Verbunden — Operations sind moeglich.
74    Connected,
75}
76
77/// Client-spezifische Error-Klassen.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ClientError {
80    /// Operation in falschem Lifecycle-Status (z.B. send-without-connect).
81    InvalidState,
82    /// Transport-Layer-Fehler (Connection-Reset etc.).
83    Transport,
84    /// Submessage konnte nicht eingereiht werden (Wire-Cap erreicht).
85    QueueFull,
86}
87
88/// Synchroner XRCE-Client.
89pub struct XrceClient<T: ClientTransport> {
90    client_key: ClientKey,
91    state: ClientState,
92    transport: T,
93    next_request_id: u16,
94    /// Gepufferte ausgehende Submessages, die der Caller per
95    /// [`XrceClient::flush`] in das Transport-Layer schiebt.
96    out_queue: Vec<Submessage>,
97}
98
99impl<T: ClientTransport> XrceClient<T> {
100    /// Konstruktor.
101    pub fn new(client_key: ClientKey, transport: T) -> Self {
102        Self {
103            client_key,
104            state: ClientState::Disconnected,
105            transport,
106            next_request_id: 1,
107            out_queue: Vec::new(),
108        }
109    }
110
111    /// Aktueller Lifecycle-Status.
112    #[must_use]
113    pub fn state(&self) -> ClientState {
114        self.state
115    }
116
117    /// Liefert die ClientKey.
118    #[must_use]
119    pub fn client_key(&self) -> ClientKey {
120        self.client_key
121    }
122
123    /// Initiiert den Handshake (sendet CREATE_CLIENT).
124    ///
125    /// # Errors
126    /// `InvalidState` wenn bereits connected.
127    pub fn connect(&mut self) -> Result<(), ClientError> {
128        if self.state != ClientState::Disconnected {
129            return Err(ClientError::InvalidState);
130        }
131        self.state = ClientState::Connecting;
132        Ok(())
133    }
134
135    /// Markiert den Handshake als abgeschlossen (Caller hat
136    /// STATUS_AGENT empfangen).
137    ///
138    /// # Errors
139    /// `InvalidState`.
140    pub fn mark_connected(&mut self) -> Result<(), ClientError> {
141        if self.state != ClientState::Connecting {
142            return Err(ClientError::InvalidState);
143        }
144        self.state = ClientState::Connected;
145        Ok(())
146    }
147
148    /// Submitiert eine CREATE-Operation. Gibt eine `request_id`
149    /// zurueck.
150    ///
151    /// # Errors
152    /// `InvalidState` wenn nicht connected.
153    pub fn create_object(
154        &mut self,
155        _object_id: ObjectId,
156        _representation: ObjectVariant,
157    ) -> Result<u16, ClientError> {
158        self.require_connected()?;
159        let req = self.next_request_id;
160        self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
161        // Submessage-Encoding deleguert der Caller; wir tracken nur
162        // den State.
163        Ok(req)
164    }
165
166    /// Submitiert eine DELETE-Operation.
167    ///
168    /// # Errors
169    /// `InvalidState`.
170    pub fn delete_object(&mut self, _object_id: ObjectId) -> Result<u16, ClientError> {
171        self.require_connected()?;
172        let req = self.next_request_id;
173        self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
174        Ok(req)
175    }
176
177    /// Submitiert eine WRITE_DATA-Operation.
178    ///
179    /// # Errors
180    /// `InvalidState`.
181    pub fn request_write(
182        &mut self,
183        _writer: ObjectId,
184        _payload: &[u8],
185    ) -> Result<u16, ClientError> {
186        self.require_connected()?;
187        let req = self.next_request_id;
188        self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
189        Ok(req)
190    }
191
192    /// Submitiert eine READ_DATA-Operation (Pull-basiert).
193    ///
194    /// # Errors
195    /// `InvalidState`.
196    pub fn request_read(&mut self, _reader: ObjectId) -> Result<u16, ClientError> {
197        self.require_connected()?;
198        let req = self.next_request_id;
199        self.next_request_id = self.next_request_id.wrapping_add(1).max(1);
200        Ok(req)
201    }
202
203    /// Schließt die Session — geht zurueck nach `Disconnected`.
204    pub fn disconnect(&mut self) {
205        self.state = ClientState::Disconnected;
206        self.out_queue.clear();
207    }
208
209    /// Prueft, dass der Client connected ist.
210    fn require_connected(&self) -> Result<(), ClientError> {
211        if self.state != ClientState::Connected {
212            return Err(ClientError::InvalidState);
213        }
214        Ok(())
215    }
216
217    /// Anzahl ausstehender Submessages in der Out-Queue.
218    #[must_use]
219    pub fn out_queue_len(&self) -> usize {
220        self.out_queue.len()
221    }
222
223    /// Direkten Transport-Zugriff (z.B. fuer Read-Pull-Polling).
224    pub fn transport_mut(&mut self) -> &mut T {
225        &mut self.transport
226    }
227}
228
229#[cfg(test)]
230#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
231mod tests {
232    use super::*;
233    use alloc::vec::Vec;
234    use zerodds_xrce::header::CLIENT_KEY_LEN;
235
236    /// Mock-Transport: sammelt sent payloads.
237    struct MockTransport {
238        sent: Vec<Vec<u8>>,
239    }
240    impl MockTransport {
241        fn new() -> Self {
242            Self { sent: Vec::new() }
243        }
244    }
245    impl ClientTransport for MockTransport {
246        fn send(&mut self, payload: &[u8]) -> Result<(), ClientError> {
247            self.sent.push(payload.to_vec());
248            Ok(())
249        }
250        fn try_recv(&mut self) -> Result<Option<Vec<u8>>, ClientError> {
251            Ok(None)
252        }
253    }
254
255    fn key() -> ClientKey {
256        ClientKey([0xAB; CLIENT_KEY_LEN])
257    }
258
259    #[test]
260    fn new_client_starts_disconnected() {
261        let c = XrceClient::new(key(), MockTransport::new());
262        assert_eq!(c.state(), ClientState::Disconnected);
263        assert_eq!(c.client_key(), key());
264    }
265
266    #[test]
267    fn connect_transitions_to_connecting() {
268        let mut c = XrceClient::new(key(), MockTransport::new());
269        c.connect().expect("connect");
270        assert_eq!(c.state(), ClientState::Connecting);
271    }
272
273    #[test]
274    fn mark_connected_transitions_to_connected() {
275        let mut c = XrceClient::new(key(), MockTransport::new());
276        c.connect().expect("connect");
277        c.mark_connected().expect("ack");
278        assert_eq!(c.state(), ClientState::Connected);
279    }
280
281    #[test]
282    fn double_connect_rejected() {
283        let mut c = XrceClient::new(key(), MockTransport::new());
284        c.connect().expect("first");
285        let err = c.connect().expect_err("second");
286        assert_eq!(err, ClientError::InvalidState);
287    }
288
289    #[test]
290    fn create_without_connect_rejected() {
291        let mut c = XrceClient::new(key(), MockTransport::new());
292        let oid = ObjectId::from_raw(0x0010);
293        let v = ObjectVariant::ByReference("topic-ref".into());
294        let err = c.create_object(oid, v).expect_err("disconnected");
295        assert_eq!(err, ClientError::InvalidState);
296    }
297
298    #[test]
299    fn write_without_connect_rejected() {
300        let mut c = XrceClient::new(key(), MockTransport::new());
301        let oid = ObjectId::from_raw(0x0010);
302        let err = c.request_write(oid, b"x").expect_err("disconnected");
303        assert_eq!(err, ClientError::InvalidState);
304    }
305
306    #[test]
307    fn read_without_connect_rejected() {
308        let mut c = XrceClient::new(key(), MockTransport::new());
309        let oid = ObjectId::from_raw(0x0010);
310        let err = c.request_read(oid).expect_err("disconnected");
311        assert_eq!(err, ClientError::InvalidState);
312    }
313
314    #[test]
315    fn full_lifecycle_creates_unique_request_ids() {
316        let mut c = XrceClient::new(key(), MockTransport::new());
317        c.connect().expect("conn");
318        c.mark_connected().expect("ack");
319        let oid = ObjectId::from_raw(0x0010);
320        let r1 = c
321            .create_object(oid, ObjectVariant::ByReference("a".into()))
322            .expect("create");
323        let r2 = c.delete_object(oid).expect("delete");
324        let r3 = c.request_write(oid, b"x").expect("write");
325        let r4 = c.request_read(oid).expect("read");
326        // Request-IDs sind streng monoton.
327        assert_eq!(r1, 1);
328        assert_eq!(r2, 2);
329        assert_eq!(r3, 3);
330        assert_eq!(r4, 4);
331    }
332
333    #[test]
334    fn disconnect_clears_state() {
335        let mut c = XrceClient::new(key(), MockTransport::new());
336        c.connect().expect("conn");
337        c.mark_connected().expect("ack");
338        c.disconnect();
339        assert_eq!(c.state(), ClientState::Disconnected);
340    }
341}