realflight_bridge/bridge/local/
async_impl.rs

1//! Async implementation of the local bridge for RealFlight simulator.
2
3use std::net::SocketAddr;
4use std::sync::Arc;
5use std::time::Duration;
6
7use crate::bridge::AsyncBridge;
8use crate::soap_client::AsyncSoapClient;
9use crate::soap_client::tcp_async::AsyncTcpSoapClient;
10use crate::{BridgeError, ControlInputs, SimulatorState, Statistics, StatisticsEngine};
11
12use super::encode_control_inputs;
13
14const EMPTY_BODY: &str = "";
15const DEFAULT_CONNECT_TIMEOUT: Duration = Duration::from_millis(5);
16const DEFAULT_INIT_TIMEOUT: Duration = Duration::from_secs(5);
17/// Pool pre-creates next connection to hide latency. Only one connection needed at a time.
18const DEFAULT_POOL_SIZE: usize = 1;
19
20/// Builder for AsyncLocalBridge.
21///
22/// Configure options synchronously, then call `build()` to connect.
23#[derive(Debug, Clone)]
24pub struct AsyncLocalBridgeBuilder {
25    connect_timeout: Duration,
26    init_timeout: Duration,
27    addr: SocketAddr,
28    pool_size: usize,
29}
30
31impl Default for AsyncLocalBridgeBuilder {
32    fn default() -> Self {
33        Self {
34            connect_timeout: DEFAULT_CONNECT_TIMEOUT,
35            init_timeout: DEFAULT_INIT_TIMEOUT,
36            addr: crate::DEFAULT_SIMULATOR_HOST.parse().unwrap(),
37            pool_size: DEFAULT_POOL_SIZE,
38        }
39    }
40}
41
42impl AsyncLocalBridgeBuilder {
43    /// Creates a new builder with default settings.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Sets the connection timeout for establishing TCP connections.
49    #[must_use]
50    pub fn connect_timeout(mut self, timeout: Duration) -> Self {
51        self.connect_timeout = timeout;
52        self
53    }
54
55    /// Sets the initialization timeout for waiting for the connection pool.
56    #[must_use]
57    pub fn init_timeout(mut self, timeout: Duration) -> Self {
58        self.init_timeout = timeout;
59        self
60    }
61
62    /// Sets the simulator address.
63    #[must_use]
64    pub fn addr(mut self, addr: SocketAddr) -> Self {
65        self.addr = addr;
66        self
67    }
68
69    /// Sets the connection pool size.
70    #[must_use]
71    pub fn pool_size(mut self, size: usize) -> Self {
72        self.pool_size = size;
73        self
74    }
75
76    /// Builds the AsyncLocalBridge, connecting to the simulator.
77    pub async fn build(self) -> Result<AsyncLocalBridge, BridgeError> {
78        let statistics = Arc::new(StatisticsEngine::new());
79        let soap_client = AsyncTcpSoapClient::new(
80            self.addr,
81            self.connect_timeout,
82            self.pool_size,
83            statistics.clone(),
84        )
85        .await?;
86
87        soap_client
88            .ensure_pool_initialized(self.init_timeout)
89            .await?;
90
91        Ok(AsyncLocalBridge {
92            statistics,
93            soap_client,
94        })
95    }
96}
97
98/// Async client for interacting with RealFlight simulators via RealFlight Link.
99///
100/// # Examples
101///
102/// ```no_run
103/// use realflight_bridge::{AsyncBridge, AsyncLocalBridge, ControlInputs};
104/// use std::time::Duration;
105///
106/// #[tokio::main]
107/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
108///     // Simple: use defaults
109///     let bridge = AsyncLocalBridge::new().await?;
110///
111///     // Or with custom configuration
112///     let bridge = AsyncLocalBridge::builder()
113///         .connect_timeout(Duration::from_millis(10))
114///         .build()
115///         .await?;
116///
117///     // Create sample control inputs
118///     let mut inputs = ControlInputs::default();
119///     inputs.channels[0] = 0.5; // Neutral aileron
120///     inputs.channels[2] = 1.0; // Full throttle
121///
122///     // Exchange data with the simulator
123///     let state = bridge.exchange_data(&inputs).await?;
124///     println!("Current airspeed: {:?}", state.airspeed);
125///
126///     Ok(())
127/// }
128/// ```
129pub struct AsyncLocalBridge {
130    statistics: Arc<StatisticsEngine>,
131    soap_client: AsyncTcpSoapClient,
132}
133
134impl AsyncBridge for AsyncLocalBridge {
135    async fn exchange_data(&self, control: &ControlInputs) -> Result<SimulatorState, BridgeError> {
136        let body = encode_control_inputs(control);
137        let response = self.soap_client.send_action("ExchangeData", &body).await?;
138        match response.status_code {
139            200 => crate::decoders::decode_simulator_state(&response.body),
140            _ => Err(BridgeError::SoapFault(response.fault_message())),
141        }
142    }
143
144    async fn enable_rc(&self) -> Result<(), BridgeError> {
145        self.soap_client
146            .send_action("RestoreOriginalControllerDevice", EMPTY_BODY)
147            .await?
148            .into()
149    }
150
151    async fn disable_rc(&self) -> Result<(), BridgeError> {
152        self.soap_client
153            .send_action("InjectUAVControllerInterface", EMPTY_BODY)
154            .await?
155            .into()
156    }
157
158    async fn reset_aircraft(&self) -> Result<(), BridgeError> {
159        self.soap_client
160            .send_action("ResetAircraft", EMPTY_BODY)
161            .await?
162            .into()
163    }
164}
165
166impl AsyncLocalBridge {
167    /// Creates a new AsyncLocalBridge with default settings.
168    pub async fn new() -> Result<Self, BridgeError> {
169        AsyncLocalBridgeBuilder::default().build().await
170    }
171
172    /// Returns a builder for custom configuration.
173    pub fn builder() -> AsyncLocalBridgeBuilder {
174        AsyncLocalBridgeBuilder::default()
175    }
176
177    /// Returns a snapshot of current statistics.
178    pub fn statistics(&self) -> Statistics {
179        self.statistics.snapshot()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use crate::bridge::AsyncBridge;
187    use crate::tests::soap_stub::Server;
188    use approx::assert_relative_eq;
189    use std::net::TcpListener;
190
191    fn get_available_port() -> u16 {
192        TcpListener::bind("127.0.0.1:0")
193            .unwrap()
194            .local_addr()
195            .unwrap()
196            .port()
197    }
198
199    async fn create_bridge(port: u16) -> Result<AsyncLocalBridge, BridgeError> {
200        let addr: SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
201        AsyncLocalBridge::builder()
202            .addr(addr)
203            .connect_timeout(Duration::from_millis(1000))
204            .init_timeout(Duration::from_secs(5))
205            .build()
206            .await
207    }
208
209    // ========================================================================
210    // Builder Tests
211    // ========================================================================
212
213    mod builder_tests {
214        use super::*;
215
216        #[test]
217        fn builder_default_connect_timeout() {
218            let builder = AsyncLocalBridgeBuilder::new();
219            assert_eq!(builder.connect_timeout, Duration::from_millis(5));
220        }
221
222        #[test]
223        fn builder_default_init_timeout() {
224            let builder = AsyncLocalBridgeBuilder::new();
225            assert_eq!(builder.init_timeout, Duration::from_secs(5));
226        }
227
228        #[test]
229        fn builder_default_pool_size() {
230            let builder = AsyncLocalBridgeBuilder::new();
231            assert_eq!(builder.pool_size, 1);
232        }
233
234        #[test]
235        fn builder_connect_timeout_sets_value() {
236            let builder =
237                AsyncLocalBridgeBuilder::new().connect_timeout(Duration::from_millis(100));
238            assert_eq!(builder.connect_timeout, Duration::from_millis(100));
239        }
240
241        #[test]
242        fn builder_init_timeout_sets_value() {
243            let builder = AsyncLocalBridgeBuilder::new().init_timeout(Duration::from_secs(10));
244            assert_eq!(builder.init_timeout, Duration::from_secs(10));
245        }
246
247        #[test]
248        fn builder_addr_sets_value() {
249            let addr: SocketAddr = "192.168.1.100:18083".parse().unwrap();
250            let builder = AsyncLocalBridgeBuilder::new().addr(addr);
251            assert_eq!(builder.addr, addr);
252        }
253
254        #[test]
255        fn builder_pool_size_sets_value() {
256            let builder = AsyncLocalBridgeBuilder::new().pool_size(5);
257            assert_eq!(builder.pool_size, 5);
258        }
259
260        #[test]
261        fn builder_is_cloneable() {
262            let builder = AsyncLocalBridgeBuilder::new()
263                .connect_timeout(Duration::from_millis(100))
264                .pool_size(3);
265            let cloned = builder.clone();
266            assert_eq!(cloned.connect_timeout, builder.connect_timeout);
267            assert_eq!(cloned.pool_size, builder.pool_size);
268        }
269    }
270
271    // ========================================================================
272    // Bridge Operation Tests (TCP Integration)
273    // ========================================================================
274
275    mod bridge_operations {
276        use super::*;
277
278        #[tokio::test]
279        async fn reset_aircraft_succeeds() {
280            let port = get_available_port();
281            let _server = Server::new(port, vec!["reset-aircraft-200".to_string()]);
282            let bridge = create_bridge(port).await.unwrap();
283
284            let result = bridge.reset_aircraft().await;
285            assert!(result.is_ok(), "expected Ok: {:?}", result);
286        }
287
288        #[tokio::test]
289        async fn reset_aircraft_increments_request_count() {
290            let port = get_available_port();
291            let _server = Server::new(port, vec!["reset-aircraft-200".to_string()]);
292            let bridge = create_bridge(port).await.unwrap();
293
294            bridge.reset_aircraft().await.unwrap();
295
296            let stats = bridge.statistics();
297            assert_eq!(stats.request_count, 1);
298        }
299
300        #[tokio::test]
301        async fn enable_rc_succeeds() {
302            let port = get_available_port();
303            let _server = Server::new(
304                port,
305                vec!["restore-original-controller-device-200".to_string()],
306            );
307            let bridge = create_bridge(port).await.unwrap();
308
309            let result = bridge.enable_rc().await;
310            assert!(result.is_ok(), "expected Ok: {:?}", result);
311        }
312
313        #[tokio::test]
314        async fn enable_rc_returns_soap_fault_on_500() {
315            let port = get_available_port();
316            let _server = Server::new(
317                port,
318                vec!["restore-original-controller-device-500".to_string()],
319            );
320            let bridge = create_bridge(port).await.unwrap();
321
322            let result = bridge.enable_rc().await;
323            match result {
324                Err(BridgeError::SoapFault(msg)) => {
325                    assert_eq!(msg, "Pointer to original controller device is null");
326                }
327                other => panic!("expected SoapFault, got {:?}", other),
328            }
329        }
330
331        #[tokio::test]
332        async fn disable_rc_succeeds() {
333            let port = get_available_port();
334            let _server = Server::new(
335                port,
336                vec!["inject-uav-controller-interface-200".to_string()],
337            );
338            let bridge = create_bridge(port).await.unwrap();
339
340            let result = bridge.disable_rc().await;
341            assert!(result.is_ok(), "expected Ok: {:?}", result);
342        }
343
344        #[tokio::test]
345        async fn disable_rc_returns_soap_fault_on_500() {
346            let port = get_available_port();
347            let _server = Server::new(
348                port,
349                vec!["inject-uav-controller-interface-500".to_string()],
350            );
351            let bridge = create_bridge(port).await.unwrap();
352
353            let result = bridge.disable_rc().await;
354            match result {
355                Err(BridgeError::SoapFault(msg)) => {
356                    assert_eq!(msg, "Preexisting controller reference");
357                }
358                other => panic!("expected SoapFault, got {:?}", other),
359            }
360        }
361    }
362
363    // ========================================================================
364    // Exchange Data Tests
365    // ========================================================================
366
367    mod exchange_data {
368        use super::*;
369
370        #[tokio::test]
371        async fn returns_simulator_state_on_success() {
372            let port = get_available_port();
373            let _server = Server::new(port, vec!["return-data-200".to_string()]);
374            let bridge = create_bridge(port).await.unwrap();
375
376            let control = ControlInputs::default();
377            let result = bridge.exchange_data(&control).await;
378
379            assert!(result.is_ok(), "expected Ok: {:?}", result);
380            let state = result.unwrap();
381            assert_eq!(state.current_physics_speed_multiplier, 1.0);
382        }
383
384        #[tokio::test]
385        async fn returns_soap_fault_on_500() {
386            let port = get_available_port();
387            let _server = Server::new(port, vec!["return-data-500".to_string()]);
388            let bridge = create_bridge(port).await.unwrap();
389
390            let control = ControlInputs::default();
391            let result = bridge.exchange_data(&control).await;
392
393            match result {
394                Err(BridgeError::SoapFault(msg)) => {
395                    assert_eq!(msg, "RealFlight Link controller has not been instantiated");
396                }
397                other => panic!("expected SoapFault, got {:?}", other),
398            }
399        }
400
401        #[tokio::test]
402        async fn parses_boolean_fields() {
403            let port = get_available_port();
404            let _server = Server::new(port, vec!["return-data-200".to_string()]);
405            let bridge = create_bridge(port).await.unwrap();
406
407            let state = bridge
408                .exchange_data(&ControlInputs::default())
409                .await
410                .unwrap();
411
412            assert!(!state.is_locked);
413            assert!(!state.has_lost_components);
414            assert!(state.an_engine_is_running);
415            assert!(!state.is_touching_ground);
416            assert!(state.flight_axis_controller_is_active);
417        }
418
419        #[cfg(not(feature = "uom"))]
420        #[tokio::test]
421        async fn parses_velocity_fields() {
422            let port = get_available_port();
423            let _server = Server::new(port, vec!["return-data-200".to_string()]);
424            let bridge = create_bridge(port).await.unwrap();
425
426            let state = bridge
427                .exchange_data(&ControlInputs::default())
428                .await
429                .unwrap();
430
431            assert_relative_eq!(state.airspeed, 0.040872246);
432            assert_relative_eq!(state.groundspeed, 4.643444754E-06);
433        }
434    }
435
436    // ========================================================================
437    // Statistics Tests
438    // ========================================================================
439
440    mod statistics_tests {
441        use super::*;
442
443        #[tokio::test]
444        async fn statistics_returns_snapshot() {
445            let port = get_available_port();
446            let _server = Server::new(
447                port,
448                vec![
449                    "reset-aircraft-200".to_string(),
450                    "reset-aircraft-200".to_string(),
451                ],
452            );
453            let bridge = create_bridge(port).await.unwrap();
454
455            bridge.reset_aircraft().await.unwrap();
456            bridge.reset_aircraft().await.unwrap();
457
458            let stats = bridge.statistics();
459            assert_eq!(stats.request_count, 2);
460            assert_eq!(stats.error_count, 0);
461        }
462    }
463}