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}