use std::io::BufRead;
use std::net::TcpStream;
use std::path::PathBuf;
use std::process::Stdio;
pub struct JSBSimProcessProperties {
pub executable_name: String,
pub root: PathBuf,
pub aircraft: Option<String>,
pub init_script: Option<String>,
pub script: Option<String>,
pub simulation_hz: u32,
pub suspend_on_start: bool,
pub realtime: bool,
pub port: u16,
}
impl Default for JSBSimProcessProperties {
fn default() -> Self {
JSBSimProcessProperties {
executable_name: "JSBSim".to_string(),
root: PathBuf::from("./jsbsim_root"),
aircraft: Some("Concorde".to_string()),
init_script: Some("reset00".to_string()),
script: None,
simulation_hz: 400,
suspend_on_start: true,
realtime: false,
port: 5556,
}
}
}
pub struct JSBSim {
connection: TcpStream,
process: Option<std::process::Child>,
}
#[derive(Debug)]
pub enum GetError<T: std::str::FromStr + std::fmt::Debug>
where
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
IoError(std::io::Error),
ParseError(<T as std::str::FromStr>::Err),
}
impl<T: std::str::FromStr + std::fmt::Debug> From<std::io::Error> for GetError<T>
where
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
fn from(error: std::io::Error) -> Self {
GetError::IoError(error)
}
}
impl JSBSim {
pub fn new(address: &str) -> std::io::Result<Self> {
let stream = TcpStream::connect(address)?;
let mut jsbsim = JSBSim {
connection: stream,
process: None,
};
jsbsim.read_line()?;
Ok(jsbsim)
}
pub fn new_with_process(properties: JSBSimProcessProperties) -> Result<Self, std::io::Error> {
let mut command = std::process::Command::new(properties.executable_name.as_str());
command
.stdout(Stdio::piped())
.arg(format!(
"--simulation-rate={rate}",
rate = properties.simulation_hz
))
.arg(format!("--root={root}", root = properties.root.display()));
if let Some(aircraft) = properties.aircraft {
command.arg(format!("--aircraft={aircraft}", aircraft = aircraft));
}
if let Some(init_script) = properties.init_script {
command.arg(format!("--initfile={script}", script = init_script));
}
if let Some(script) = properties.script {
command.arg(format!("--script={script}", script = script));
}
if properties.suspend_on_start {
command.arg("--suspend");
}
if properties.realtime {
command.arg("--realtime");
}
let mut process = command.spawn()?;
let stdout = process.stdout.as_mut().unwrap();
let mut reader = std::io::BufReader::new(stdout);
let mut line = String::new();
loop {
line.clear();
let bytes_read = reader.read_line(&mut line)?;
if bytes_read == 0 {
break; }
if line.contains("JSBSim Execution beginning") {
break;
}
}
let address = format!("localhost:{port}", port = properties.port);
match TcpStream::connect(address) {
Ok(stream) => {
let mut jsbsim = JSBSim {
connection: stream,
process: Some(process),
};
jsbsim.read_line()?;
return Ok(jsbsim);
}
Err(e) => {
let _ = process.kill();
let _ = process.wait();
return Err(e);
}
}
}
fn read_line(&mut self) -> std::io::Result<String> {
let mut reader = std::io::BufReader::new(&self.connection);
let mut response = String::new();
reader.read_line(&mut response)?;
while response.trim().is_empty() || response.trim() == "JSBSim>" {
response.clear();
reader.read_line(&mut response)?;
}
Ok(response)
}
pub fn hold(&mut self) -> std::io::Result<()> {
self.send_command("hold\n")?;
self.read_line().map(|_| ())
}
pub fn resume(&mut self) -> std::io::Result<()> {
self.send_command("resume\n")?;
let line = self.read_line()?;
if !line.trim().ends_with("Resuming") {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to resume: {}", line.trim()),
));
}
Ok(())
}
pub fn iterate(&mut self, steps: i32) -> std::io::Result<()> {
use std::io::Write;
self.connection
.write_all(format!("iterate {steps}\n", steps = steps).as_bytes())?;
let line = self.read_line()?;
if !line.trim().ends_with("Iterations performed") {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to iterate: {}", line.trim()),
));
}
Ok(())
}
pub fn set(&mut self, key: &str, value: impl std::fmt::Display) -> std::io::Result<()> {
use std::io::Write;
self.connection
.write_all(format!("set {key} {value}\n").as_bytes())?;
let line = self.read_line()?;
if !line.trim().ends_with("set successful") {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to set property: {}", line.trim()),
));
}
Ok(())
}
pub fn get<T: std::str::FromStr + std::fmt::Debug>(
&mut self,
key: &str,
) -> Result<T, GetError<T>>
where
<T as std::str::FromStr>::Err: std::fmt::Debug,
{
use std::io::Write;
self.connection
.write_all(format!("get {key}\n").as_bytes())?;
let response = self.read_line()?;
let parts = response.trim().split("=");
let collection = parts.collect::<Vec<&str>>();
debug_assert!(
collection.len() == 2,
"Response from JSBSim not in expected format '{}' '{}'",
collection.len(),
response.trim()
);
collection
.get(1)
.ok_or_else(|| {
GetError::IoError(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"No value returned",
))
})?
.trim()
.parse::<T>()
.map_err(GetError::ParseError)
}
fn send_command(&mut self, command: &str) -> std::io::Result<()> {
use std::io::Write;
self.connection.write_all(command.as_bytes())?;
Ok(())
}
}
impl Drop for JSBSim {
fn drop(&mut self) {
let _ = self.send_command("quit\n");
if let Some(mut process) = self.process.take() {
process.kill().ok();
let exit_code = process.wait();
if !exit_code.map(|code| code.success()).unwrap_or(false) {
eprintln!("Warning: JSBSim process did not exit cleanly");
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn jsbsim_connection_performs_as_expected() {
let properties = JSBSimProcessProperties {
simulation_hz: 400,
..Default::default()
};
let mut jsbsim =
JSBSim::new_with_process(properties).expect("Failed to start JSBSim process");
let time: i32 = jsbsim
.get("simulation/cycle_duration")
.expect("Failed to get time");
assert_eq!(time, 0);
let running_engine: i32 = jsbsim
.get("propulsion/engine/set-running")
.expect("Failed to get engine running state");
assert_eq!(running_engine, 0);
assert_eq!(
jsbsim
.get::<f64>("fcs/throttle-cmd-norm")
.expect("Failed to get throttle"),
0.0
);
jsbsim
.set("fcs/throttle-cmd-norm", 1.0)
.expect("Failed to set throttle");
let throttle: f64 = jsbsim
.get("fcs/throttle-cmd-norm")
.expect("Failed to get throttle");
assert_eq!(throttle, 1.0);
assert_eq!(
jsbsim
.get::<f64>("simulation/sim-time-sec")
.expect("Failed to get time"),
0.0025
);
jsbsim.iterate(120).expect("Failed to iterate");
std::thread::sleep(std::time::Duration::from_millis(100));
assert_eq!(
jsbsim
.get::<f64>("simulation/sim-time-sec")
.expect("Failed to get time"),
0.3025
);
jsbsim.resume().expect("Failed to resume");
std::thread::sleep(std::time::Duration::from_millis(100));
assert_ne!(
jsbsim
.get::<f64>("simulation/sim-time-sec")
.expect("Failed to get time"),
0.3025
);
}
}