realflight_bridge/bridge/
local.rs

1use std::{error::Error, sync::Arc, time::Duration};
2
3use crate::{
4    ControlInputs, SimulatorState, SoapResponse, Statistics, StatisticsEngine, TcpSoapClient,
5};
6
7#[cfg(test)]
8use crate::StubSoapClient;
9
10use super::RealFlightBridge;
11
12const EMPTY_BODY: &str = "";
13
14/// A high-level client for interacting with RealFlight simulators via RealFlight Link.
15///
16/// # Overview
17///
18/// [RealFlightBridge] is your main entry point to controlling and querying the
19/// RealFlight simulator. It exposes methods to:
20///
21/// - Send flight control inputs (e.g., RC channel data).
22/// - Retrieve real-time flight state from the simulator.
23/// - Toggle between internal and external RC control devices.
24/// - Reset aircraft position and orientation.
25///
26/// # Examples
27///
28/// ```no_run
29/// use realflight_bridge::{RealFlightBridge, RealFlightLocalBridge, Configuration, ControlInputs};
30/// use std::error::Error;
31///
32/// fn main() -> Result<(), Box<dyn Error>> {
33///     // Build a RealFlightBridge client
34///     let bridge = RealFlightLocalBridge::new()?;
35///
36///     // Create sample control inputs
37///     let mut inputs = ControlInputs::default();
38///     inputs.channels[0] = 0.5; // Neutral aileron
39///     inputs.channels[1] = 0.5; // Neutral elevator
40///     inputs.channels[2] = 1.0; // Full throttle
41///
42///     // Enable external control
43///     bridge.disable_rc()?;
44///
45///     // Exchange data with the simulator
46///     let sim_state = bridge.exchange_data(&inputs)?;
47///     println!("Current airspeed: {:?}", sim_state.airspeed);
48///
49///     // Return to internal control
50///     bridge.enable_rc()?;
51///
52///     Ok(())
53/// }
54/// ```
55///
56/// # Error Handling
57///
58/// Methods that exchange data or mutate simulator state return `Result<T, Box<dyn Error>>`.
59/// Common errors include:
60///
61/// - Connection timeouts
62/// - SOAP faults (e.g., simulator not ready or invalid commands)
63/// - Parsing issues for responses
64///
65/// Any non-2xx HTTP status code will typically return an error containing the simulator’s
66/// fault message, if available.
67///
68/// # Statistics
69///
70/// Use [`statistics()`](#method.statistics) to retrieve current performance metrics
71/// such as request count, errors, and average frame rate. This is useful for profiling
72/// real-time loops or detecting dropped messages.
73pub struct RealFlightLocalBridge {
74    statistics: Arc<StatisticsEngine>,
75    soap_client: Box<dyn SoapClient>,
76}
77
78impl RealFlightBridge for RealFlightLocalBridge {
79    /// Exchanges flight control data with the RealFlight simulator.
80    ///
81    /// This method transmits the provided [ControlInputs] (e.g., RC channel values)
82    /// to the RealFlight simulator and retrieves an updated [SimulatorState] in return,
83    /// including position, orientation, velocities, and more.
84    ///
85    /// # Parameters
86    ///
87    /// - `control`: A [ControlInputs] struct specifying up to 12 RC channels (0.0–1.0 range).
88    ///
89    /// # Returns
90    ///
91    /// A [Result] with the updated [SimulatorState] on success, or an error if
92    /// something goes wrong (e.g., SOAP fault, network timeout).
93    ///
94    /// # Examples
95    ///
96    /// ```no_run
97    /// use realflight_bridge::{RealFlightBridge, RealFlightLocalBridge, Configuration, ControlInputs};
98    /// use std::error::Error;
99    ///
100    /// fn main() -> Result<(), Box<dyn Error>> {
101    ///     let bridge = RealFlightLocalBridge::new()?;
102    ///
103    ///     // Create sample control inputs
104    ///     let mut inputs = ControlInputs::default();
105    ///     inputs.channels[0] = 0.5; // Aileron neutral
106    ///     inputs.channels[2] = 1.0; // Full throttle
107    ///
108    ///     // Exchange data with the simulator
109    ///     let state = bridge.exchange_data(&inputs)?;
110    ///     println!("Current airspeed: {:?}", state.airspeed);
111    ///     println!("Altitude above ground: {:?}", state.altitude_agl);
112    ///
113    ///     Ok(())
114    /// }
115    /// ```
116    fn exchange_data(&self, control: &ControlInputs) -> Result<SimulatorState, Box<dyn Error>> {
117        let body = encode_control_inputs(control);
118        let response = self.soap_client.send_action("ExchangeData", &body)?;
119        match response.status_code {
120            200 => crate::decoders::decode_simulator_state(&response.body),
121            _ => Err(crate::decode_fault(&response).into()),
122        }
123    }
124
125    /// Reverts the RealFlight simulator to use its original Spektrum (or built-in) RC input.
126    ///
127    /// Calling [RealFlightBridge::enable_rc] instructs RealFlight to restore its native RC controller
128    /// device (e.g., Spektrum). Once enabled, external RC control via the RealFlight Link
129    /// interface is disabled until you explicitly call [RealFlightBridge::disable_rc].
130    ///
131    /// # Returns
132    ///
133    /// `Ok(())` if the simulator successfully reverts to using the original RC controller.
134    /// An `Err`` is returned if RealFlight cannot locate or restore the original controller device.
135    ///
136    /// # Examples
137    ///
138    /// ```no_run
139    /// use realflight_bridge::{RealFlightBridge, RealFlightLocalBridge, Configuration};
140    /// use std::error::Error;
141    ///
142    /// fn main() -> Result<(), Box<dyn Error>> {
143    ///     let bridge = RealFlightLocalBridge::new()?;
144    ///
145    ///     // Switch back to native Spektrum controller
146    ///     bridge.enable_rc()?;
147    ///
148    ///     // The simulator is now using its default RC input
149    ///
150    ///     Ok(())
151    /// }
152    /// ```
153    fn enable_rc(&self) -> Result<(), Box<dyn Error>> {
154        self.soap_client
155            .send_action("RestoreOriginalControllerDevice", EMPTY_BODY)?
156            .into()
157    }
158
159    /// Switches the RealFlight simulator’s input to the external RealFlight Link controller,
160    /// effectively disabling any native Spektrum (or other built-in) RC device.
161    ///
162    /// Once [RealFlightBridge::disable_rc] is called, RealFlight listens exclusively for commands sent
163    /// through this external interface. To revert to the original RC device, call
164    /// [RealFlightBridge::enable_rc].
165    ///
166    /// # Returns
167    ///
168    /// Returns `Ok(())` if RealFlight Link mode is successfully activated, or an `Err` if
169    /// the request fails (e.g., simulator is not ready or rejects the command).
170    ///
171    /// # Examples
172    ///
173    /// ```no_run
174    /// use realflight_bridge::{RealFlightBridge, RealFlightLocalBridge, Configuration};
175    /// use std::error::Error;
176    ///
177    /// fn main() -> Result<(), Box<dyn Error>> {
178    ///     let bridge = RealFlightLocalBridge::new()?;
179    ///
180    ///     // Switch to the external RealFlight Link input
181    ///     bridge.disable_rc()?;
182    ///
183    ///     // Now the simulator expects input via RealFlight Link
184    ///
185    ///     Ok(())
186    /// }
187    /// ```
188    fn disable_rc(&self) -> Result<(), Box<dyn Error>> {
189        self.soap_client
190            .send_action("InjectUAVControllerInterface", EMPTY_BODY)?
191            .into()
192    }
193
194    /// Resets the currently loaded aircraft in the RealFlight simulator, analogous
195    /// to pressing the spacebar in the simulator’s interface.
196    ///
197    /// This call repositions the aircraft back to its initial state and orientation,
198    /// clearing any damage or off-runway positioning. It’s useful for rapid iteration
199    /// when testing control loops or flight maneuvers.
200    ///
201    /// # Returns
202    ///
203    /// `Ok(())` upon a successful reset. Returns an error if RealFlight rejects the command
204    /// or if a network issue prevents delivery.
205    ///
206    /// # Examples
207    ///
208    /// ```no_run
209    /// use realflight_bridge::{RealFlightBridge, RealFlightLocalBridge, Configuration};
210    /// use std::error::Error;
211    ///
212    /// fn main() -> Result<(), Box<dyn Error>> {
213    ///     let bridge = RealFlightLocalBridge::new()?;
214    ///
215    ///     // Perform a flight test...
216    ///     // ...
217    ///
218    ///     // Reset the aircraft to starting conditions:
219    ///     bridge.reset_aircraft()?;
220    ///     Ok(())
221    /// }
222    /// ```
223    fn reset_aircraft(&self) -> Result<(), Box<dyn Error>> {
224        self.soap_client
225            .send_action("ResetAircraft", EMPTY_BODY)?
226            .into()
227    }
228}
229
230impl RealFlightLocalBridge {
231    /// Creates a new [RealFlightBridge] instance configured to communicate
232    /// with a RealFlight simulator running on local machine.
233    ///
234    /// # Returns
235    ///
236    /// A [Result] containing a fully initialized [RealFlightBridge] if the TCP connection
237    /// pool is successfully created. Returns an error if the simulator address cannot be
238    /// resolved or if the pool could not be initialized.
239    ///
240    /// # Examples
241    ///
242    /// ```no_run
243    /// use realflight_bridge::{RealFlightLocalBridge, Configuration};
244    /// use std::error::Error;
245    ///
246    /// fn main() -> Result<(), Box<dyn Error>> {
247    ///     // Build a bridge to the RealFlight simulator.
248    ///     // Connects to simulator at 127.0.0.1:18083
249    ///     let bridge = RealFlightLocalBridge::new()?;
250    ///
251    ///     // Now you can interact with RealFlight:
252    ///     // - Send/receive flight control data
253    ///     // - Reset aircraft
254    ///     // - Toggle RC input
255    ///
256    ///     Ok(())
257    /// }
258    /// ```
259    ///
260    /// # Errors
261    ///
262    /// This function will return an error in the following situations:
263    ///
264    /// - If the TCP connection pool cannot be established (e.g., RealFlight is not running).
265    pub fn new() -> Result<RealFlightLocalBridge, Box<dyn Error>> {
266        let statistics = Arc::new(StatisticsEngine::new());
267        let soap_client = TcpSoapClient::new(Configuration::default(), statistics.clone())?;
268        soap_client.ensure_pool_initialized()?;
269
270        Ok(RealFlightLocalBridge {
271            statistics: statistics.clone(),
272            soap_client: Box::new(soap_client),
273        })
274    }
275
276    /// Creates a new [RealFlightBridge] instance configured to communicate
277    /// with a RealFlight simulator using a TCP-based Soap client.
278    ///
279    /// # Parameters
280    ///
281    /// - `configuration`: A [Configuration] specifying simulator address, connection
282    ///   timeouts, and the number of pooled connections.
283    ///
284    /// # Returns
285    ///
286    /// A [Result] containing a fully initialized [RealFlightBridge] if the TCP connection
287    /// pool is successfully created. Returns an error if the simulator address cannot be
288    /// resolved or if the pool could not be initialized.
289    ///
290    /// # Examples
291    ///
292    /// ```no_run
293    /// use realflight_bridge::{RealFlightLocalBridge, Configuration};
294    /// use std::error::Error;
295    ///
296    /// fn main() -> Result<(), Box<dyn Error>> {
297    ///     // Use default localhost-based config.
298    ///     let config = Configuration::default();
299    ///
300    ///     // Build a bridge to the RealFlight simulator.
301    ///     let bridge = RealFlightLocalBridge::with_configuration(&config)?;
302    ///
303    ///     // Now you can interact with RealFlight:
304    ///     // - Send/receive flight control data
305    ///     // - Reset aircraft
306    ///     // - Toggle RC input
307    ///
308    ///     Ok(())
309    /// }
310    /// ```
311    ///
312    /// # Errors
313    ///
314    /// This function will return an error in the following situations:
315    ///
316    /// - If the simulator address specified in `configuration` is invalid.
317    /// - If the TCP connection pool cannot be established (e.g., RealFlight is not running).
318    pub fn with_configuration(
319        configuration: &Configuration,
320    ) -> Result<RealFlightLocalBridge, Box<dyn Error>> {
321        let statistics = Arc::new(StatisticsEngine::new());
322        let soap_client = TcpSoapClient::new(configuration.clone(), statistics.clone())?;
323        soap_client.ensure_pool_initialized()?;
324
325        Ok(RealFlightLocalBridge {
326            statistics: statistics.clone(),
327            soap_client: Box::new(soap_client),
328        })
329    }
330
331    /// Creates a new RealFlightLink client
332    /// simulator_url: the url to the RealFlight simulator
333    #[cfg(test)]
334    pub(crate) fn stub(
335        mut soap_client: StubSoapClient,
336    ) -> Result<RealFlightLocalBridge, Box<dyn Error>> {
337        let statistics = Arc::new(StatisticsEngine::new());
338
339        soap_client.statistics = Some(statistics.clone());
340
341        Ok(RealFlightLocalBridge {
342            statistics,
343            soap_client: Box::new(soap_client),
344        })
345    }
346
347    #[cfg(test)]
348    pub fn requests(&self) -> Vec<String> {
349        self.soap_client.requests().clone()
350    }
351
352    /// Get statistics for the RealFlightBridge
353    pub fn statistics(&self) -> Statistics {
354        self.statistics.snapshot()
355    }
356}
357
358pub(crate) trait SoapClient {
359    fn send_action(&self, action: &str, body: &str) -> Result<SoapResponse, Box<dyn Error>>;
360    #[cfg(test)]
361    fn requests(&self) -> Vec<String> {
362        Vec::new()
363    }
364}
365
366const CONTROL_INPUTS_CAPACITY: usize = 291;
367
368pub(crate) fn encode_envelope(action: &str, body: &str) -> String {
369    let mut envelope = String::with_capacity(200 + body.len());
370
371    envelope.push_str("<?xml version='1.0' encoding='UTF-8'?>");
372    envelope.push_str("<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>");
373    envelope.push_str("<soap:Body>");
374    envelope.push_str(&format!("<{}>{}</{}>", action, body, action));
375    envelope.push_str("</soap:Body>");
376    envelope.push_str("</soap:Envelope>");
377
378    envelope
379}
380
381pub(crate) fn encode_control_inputs(inputs: &ControlInputs) -> String {
382    let mut message = String::with_capacity(CONTROL_INPUTS_CAPACITY);
383
384    message.push_str("<pControlInputs>");
385    message.push_str("<m-selectedChannels>4095</m-selectedChannels>");
386    //message.push_str("<m-selectedChannels>0</m-selectedChannels>");
387    message.push_str("<m-channelValues-0to1>");
388    for num in inputs.channels.iter() {
389        message.push_str(&format!("<item>{}</item>", num));
390    }
391    message.push_str("</m-channelValues-0to1>");
392    message.push_str("</pControlInputs>");
393
394    message
395}
396
397/// Configuration settings for the RealFlight Link bridge.
398///
399/// The Configuration struct controls how the bridge connects to and communicates with
400/// the RealFlight simulator. It provides settings for connection management, timeouts,
401/// and performance optimization.
402///
403/// # Connection Pool
404///
405/// The bridge maintains a pool of TCP connections to improve performance when making
406/// frequent SOAP requests. The pool size and connection behavior can be tuned using
407/// the `buffer_size`, `connect_timeout`, and `retry_delay` parameters.
408///
409/// # Default Configuration
410///
411/// The default configuration is suitable for most local development:
412/// ```rust
413/// use realflight_bridge::Configuration;
414/// use std::time::Duration;
415///
416/// let default_config = Configuration {
417///     simulator_host: "127.0.0.1:18083".to_string(),
418///     connect_timeout: Duration::from_millis(50),
419///     pool_size: 1,
420/// };
421/// ```
422///
423/// # Examples
424///
425/// Basic configuration for local development:
426/// ```rust
427/// use realflight_bridge::Configuration;
428/// use std::time::Duration;
429///
430/// let config = Configuration::default();
431/// ```
432///
433/// Configuration optimized for high-frequency control:
434/// ```rust
435/// use realflight_bridge::Configuration;
436/// use std::time::Duration;
437///
438/// let config = Configuration {
439///     simulator_host: "127.0.0.1:18083".to_string(),
440///     connect_timeout: Duration::from_millis(25),  // Faster timeout
441///     pool_size: 5,                                // Larger connection pool
442/// };
443/// ```
444///
445/// Configuration for a different network interface:
446/// ```rust
447/// use realflight_bridge::Configuration;
448/// use std::time::Duration;
449///
450/// let config = Configuration {
451///     simulator_host: "192.168.1.100:18083".to_string(),
452///     connect_timeout: Duration::from_millis(100), // Longer timeout for network
453///     pool_size: 2,
454/// };
455/// ```
456#[derive(Clone, Debug)]
457pub struct Configuration {
458    /// The host where the RealFlight simulator is listening for connections.
459    ///
460    /// # Format
461    /// The value should be in the format "host:port". For local development,
462    /// this is typically "127.0.0.1:18083".
463    ///
464    /// # Important Notes
465    /// * The bridge should run on the same machine as RealFlight for best performance
466    /// * Remote connections may experience significant latency due to SOAP overhead
467    pub simulator_host: String,
468
469    /// Maximum time to wait when establishing a new TCP connection.
470    ///
471    /// # Performance Impact
472    /// * Lower values improve responsiveness when the simulator is unavailable
473    /// * Too low values may cause unnecessary connection failures
474    /// * Recommended range: 25-100ms for local connections
475    ///
476    /// # Default
477    /// 5 milliseconds
478    pub connect_timeout: Duration,
479
480    /// Size of the connection pool.
481    ///
482    /// The connection pool maintains a set of pre-established TCP connections
483    /// to improve performance when making frequent requests to the simulator.
484    ///
485    /// # Performance Impact
486    /// * Larger values can improve throughput for frequent state updates
487    /// * Too large values may waste system resources
488    /// * Recommended range: 1-5 connections
489    ///
490    /// # Memory Usage
491    /// Each connection in the pool consumes system resources:
492    /// * TCP socket
493    /// * Memory for connection management
494    /// * System file descriptors
495    ///
496    /// # Default
497    /// 1 connection
498    pub pool_size: usize,
499}
500
501impl Default for Configuration {
502    fn default() -> Self {
503        Configuration {
504            simulator_host: "127.0.0.1:18083".to_string(),
505            connect_timeout: Duration::from_millis(5),
506            pool_size: 1,
507        }
508    }
509}