From 6d8fc61c0b25b39f0f0a8f427a29678f3c18fcb1 Mon Sep 17 00:00:00 2001 From: Mihai Dinculescu Date: Sat, 8 Oct 2022 19:09:57 +0100 Subject: [PATCH] Define the desired API --- Cargo.lock | 96 ++++++++++++ Cargo.toml | 4 + README.md | 8 +- SimConnect.dll | Bin 0 -> 62464 bytes examples/basic.rs | 102 ++++++++++++ examples/logging.rs | 18 +++ src/domain/airport_data.rs | 7 + src/domain/condition.rs | 5 + src/domain/data_type.rs | 5 + src/domain/event.rs | 19 +++ src/domain/group.rs | 5 + src/domain/mod.rs | 15 ++ src/domain/notification.rs | 28 ++++ src/domain/period.rs | 5 + src/errors.rs | 15 ++ src/lib.rs | 309 ++----------------------------------- src/macros.rs | 29 ++++ src/simconnect.rs | 267 ++++++++++++++++++++++++++++++++ src/simconnect_object.rs | 5 + 19 files changed, 642 insertions(+), 300 deletions(-) create mode 100644 SimConnect.dll create mode 100644 examples/basic.rs create mode 100644 examples/logging.rs create mode 100644 src/domain/airport_data.rs create mode 100644 src/domain/condition.rs create mode 100644 src/domain/data_type.rs create mode 100644 src/domain/event.rs create mode 100644 src/domain/group.rs create mode 100644 src/domain/mod.rs create mode 100644 src/domain/notification.rs create mode 100644 src/domain/period.rs create mode 100644 src/errors.rs create mode 100644 src/macros.rs create mode 100644 src/simconnect.rs create mode 100644 src/simconnect_object.rs diff --git a/Cargo.lock b/Cargo.lock index 1acc99c..d85bef0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,6 +200,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.5.0" @@ -222,6 +231,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num_enum" version = "0.5.7" @@ -255,6 +274,12 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "peeking_take_while" version = "0.1.2" @@ -307,6 +332,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.27" @@ -325,6 +359,15 @@ version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.1.0" @@ -337,9 +380,17 @@ version = "0.1.0" dependencies = [ "bindgen", "num_enum", + "thiserror", "tracing", + "tracing-subscriber", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "strsim" version = "0.10.0" @@ -392,6 +443,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "toml" version = "0.5.9" @@ -431,6 +491,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -439,6 +529,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "which" version = "4.3.0" diff --git a/Cargo.toml b/Cargo.toml index 2ef7fb0..702575c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,3 +12,7 @@ bindgen = "0.60" [dependencies] num_enum = "0.5" tracing = "0.1" +thiserror = "1.0" + +[dev-dependencies] +tracing-subscriber = { version = "0.3", features = [ "env-filter" ] } diff --git a/README.md b/README.md index 8f5eb0c..6f5f3b9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,9 @@ # SimConnect SDK in Rust -WIP +## Running the examples + +```bash +git clone git@github.com:mihai-dinculescu/simconnect-sdk.git +cd simconnect-sdk +RUST_LOG=info cargo run --example basic +``` diff --git a/SimConnect.dll b/SimConnect.dll new file mode 100644 index 0000000000000000000000000000000000000000..6b25958dd7254d52da008268f3a95284e051773e GIT binary patch literal 62464 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 WVt8W5#J-8gCltRzFMs|Yd*DBU)&e8| literal 0 HcmV?d00001 diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..293b328 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,102 @@ +use logging::setup_logging; +use simconnect_sdk::{ + ConditionEnum, DataType, Notification, NotificationData, PeriodEnum, SimConnect, + SimConnectError, +}; +use tracing::{error, info}; + +mod logging; + +#[derive(Debug, Clone)] +pub struct GpsData { + pub lat: f64, + pub lon: f64, + pub alt: f64, + pub gps_ground_magnetic_track: f64, + pub gps_ground_speed: f64, +} + +impl simconnect_sdk::SimConnectObject for GpsData { + fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError> { + client.add_to_data_definition(id, "PLANE LATITUDE", "degrees", DataType::F64)?; + client.add_to_data_definition(id, "PLANE LONGITUDE", "degrees", DataType::F64)?; + client.add_to_data_definition(id, "PLANE ALTITUDE", "meters", DataType::F64)?; + client.add_to_data_definition(id, "GPS GROUND MAGNETIC TRACK", "degrees", DataType::F64)?; + client.add_to_data_definition( + id, + "GPS GROUND SPEED", + "Meters per second", + DataType::F64, + )?; + client.request_data_on_sim_object(id, PeriodEnum::Second, ConditionEnum::None)?; + + Ok(()) + } +} + +impl TryFrom<&NotificationData> for GpsData { + type Error = (); + + fn try_from(value: &NotificationData) -> Result { + value.try_into::().ok_or(()) + } +} + +#[derive(Debug, Clone)] +pub struct OnGround { + pub sim_on_ground: bool, +} + +impl simconnect_sdk::SimConnectObject for OnGround { + fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError> { + client.add_to_data_definition(id, "SIM ON GROUND", "bool", DataType::Bool)?; + client.request_data_on_sim_object(id, PeriodEnum::Second, ConditionEnum::None)?; + + Ok(()) + } +} + +impl TryFrom<&NotificationData> for OnGround { + type Error = (); + + fn try_from(value: &NotificationData) -> Result { + value.try_into::().ok_or(()) + } +} + +fn main() -> Result<(), Box> { + setup_logging()?; + + let client = SimConnect::new("Simple Program"); + + match client { + Ok(mut client) => loop { + let notification = client.get_next_dispatch()?; + + match notification { + Some(Notification::Open) => { + info!("Open"); + + client.register_object::()?; + client.register_object::()?; + } + Some(Notification::Data(data)) => { + if let Ok(gps_data) = GpsData::try_from(&data) { + info!("GPS Data: {:?}", gps_data); + continue; + } + if let Ok(on_ground) = OnGround::try_from(&data) { + info!("On Ground data: {:?}", on_ground); + continue; + } + } + _ => (), + } + }, + Err(e) => { + error!("{e:?}") + } + } + + Ok(()) +} diff --git a/examples/logging.rs b/examples/logging.rs new file mode 100644 index 0000000..36b7028 --- /dev/null +++ b/examples/logging.rs @@ -0,0 +1,18 @@ +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +pub fn setup_logging() -> Result<(), Box> { + let filter_layer = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("info"))?; + let fmt_layer = fmt::layer() + .with_target(false) + .with_span_events(fmt::format::FmtSpan::FULL); + + tracing_subscriber::registry() + .with(filter_layer) + .with(fmt_layer) + .init(); + + Ok(()) +} + +#[allow(dead_code)] +fn main() {} diff --git a/src/domain/airport_data.rs b/src/domain/airport_data.rs new file mode 100644 index 0000000..4f9498e --- /dev/null +++ b/src/domain/airport_data.rs @@ -0,0 +1,7 @@ +#[derive(Debug, Clone)] +pub struct AirportData { + pub icao: String, + pub lat: f64, + pub lon: f64, + pub alt: f64, +} diff --git a/src/domain/condition.rs b/src/domain/condition.rs new file mode 100644 index 0000000..f69b3fd --- /dev/null +++ b/src/domain/condition.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone)] +pub enum ConditionEnum { + None, + Changed, +} diff --git a/src/domain/data_type.rs b/src/domain/data_type.rs new file mode 100644 index 0000000..a3db80f --- /dev/null +++ b/src/domain/data_type.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone)] +pub enum DataType { + F64, + Bool, +} diff --git a/src/domain/event.rs b/src/domain/event.rs new file mode 100644 index 0000000..ad30f3e --- /dev/null +++ b/src/domain/event.rs @@ -0,0 +1,19 @@ +use std::os::raw::c_char; + +#[derive(Debug, Copy, Clone, num_enum::TryFromPrimitive)] +#[repr(u32)] +pub enum Event { + Brakes, + BrakesLeft, + AxisLeftBrakeSet, +} + +impl Event { + pub(crate) fn into_c_char(self) -> *const c_char { + match self { + Event::Brakes => "BRAKES\0".as_ptr() as *const c_char, + Event::BrakesLeft => "BRAKES_LEFT\0".as_ptr() as *const c_char, + Event::AxisLeftBrakeSet => "AXIS_LEFT_BRAKE_SET\0".as_ptr() as *const c_char, + } + } +} diff --git a/src/domain/group.rs b/src/domain/group.rs new file mode 100644 index 0000000..b246c9f --- /dev/null +++ b/src/domain/group.rs @@ -0,0 +1,5 @@ +#[derive(Copy, Clone)] +#[repr(u32)] +pub enum Group { + Group0, +} diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..79096b6 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,15 @@ +mod airport_data; +mod condition; +mod data_type; +mod event; +mod group; +mod notification; +mod period; + +pub use airport_data::*; +pub use condition::*; +pub use data_type::*; +pub use event::*; +pub use group::*; +pub use notification::*; +pub use period::*; diff --git a/src/domain/notification.rs b/src/domain/notification.rs new file mode 100644 index 0000000..ff3eb59 --- /dev/null +++ b/src/domain/notification.rs @@ -0,0 +1,28 @@ +use crate::{AirportData, Event, SimConnectObject}; + +pub enum Notification { + Open, + Event(Event), + Data(NotificationData), + AirportList(Vec), + Quit, + Exception(u32), +} + +pub struct NotificationData { + pub(crate) type_id: String, + pub(crate) data_addr: *const u32, +} + +impl NotificationData { + pub fn try_into(&self) -> Option { + let type_id: String = std::any::type_name::().into(); + + if self.type_id == type_id { + let data: &T = unsafe { std::mem::transmute_copy(&self.data_addr) }; + Some(data.clone()) + } else { + None + } + } +} diff --git a/src/domain/period.rs b/src/domain/period.rs new file mode 100644 index 0000000..1f6fbff --- /dev/null +++ b/src/domain/period.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone)] +pub enum PeriodEnum { + VisualFrame { interval: u32 }, + Second, +} diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 0000000..5f61754 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,15 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum SimConnectError { + #[error("SimConnect error: {0}")] + SimConnectError(i32), + #[error("SimConnect unrecognized: {0}")] + SimConnectUnrecognizedEvent(u32), + #[error("Object `{0}` has already been registered")] + ObjectAlreadyRegistered(String), + #[error("Conversion error: {0}")] + ConversionError(#[from] std::num::TryFromIntError), + #[error("Unexpected error: {0}")] + UnexpectedError(String), +} diff --git a/src/lib.rs b/src/lib.rs index 14e43bd..2d51cce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,303 +1,14 @@ -use helpers::fixed_c_str_to_string; -use std::ffi::c_void; -use std::{convert::TryFrom, fmt::Debug}; - mod bindings; +mod domain; +mod errors; mod helpers; +mod macros; +mod simconnect; +mod simconnect_object; -macro_rules! success { - ($hr:expr) => {{ - let hr = $hr; - if hr != 0 { - return Err(hr); - } - }}; -} +pub(crate) use macros::{as_c_string, ok_if_fail, success}; -macro_rules! ok_if_fail { - ($hr:expr, $ret:expr) => {{ - let hr = $hr; - let ret = $ret; - if hr != 0 { - return Ok(ret); - } - }}; -} - -macro_rules! as_c_string { - ($target:expr) => { - std::ffi::CString::new($target) - .expect("failed to create CString") - .as_ptr() - }; -} - -#[derive(Debug)] -pub struct SimConnect { - pub handle: std::ptr::NonNull, -} - -impl SimConnect { - #[tracing::instrument(name = "SimConnect::new")] - pub fn new(name: &str) -> Result { - let mut handle = std::ptr::null_mut(); - - success!(unsafe { - bindings::SimConnect_Open( - &mut handle, - as_c_string!(name), - std::ptr::null_mut(), - 0, - std::ptr::null_mut(), - 0, - ) - }); - - Ok(Self { - handle: std::ptr::NonNull::new(handle) - .expect("ERROR: SimConnect_Open returned null pointer on success"), - }) - } - - #[tracing::instrument(name = "SimConnect::register_event")] - pub fn register_event(&self, event: Event) -> Result<(), i32> { - success!(unsafe { - bindings::SimConnect_MapClientEventToSimEvent( - self.handle.as_ptr(), - event as u32, - event.into_c_char(), - ) - }); - - let group = Group::Group0; - - success!(unsafe { - bindings::SimConnect_AddClientEventToNotificationGroup( - self.handle.as_ptr(), - group as u32, - event as u32, - 0, - ) - }); - - success!(unsafe { - bindings::SimConnect_SetNotificationGroupPriority(self.handle.as_ptr(), group as u32, 1) - }); - - Ok(()) - } - - #[tracing::instrument(name = "SimConnect::add_to_data_definition")] - pub fn add_to_data_definition( - &self, - request_id: u32, - datum_name: &str, - units_name: &str, - ) -> Result<(), i32> { - unsafe { - success!(bindings::SimConnect_AddToDataDefinition( - self.handle.as_ptr(), - request_id, - as_c_string!(datum_name), - as_c_string!(units_name), - bindings::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_FLOAT64, - 0.0, - u32::MAX, - )); - } - - Ok(()) - } - - #[tracing::instrument(name = "SimConnect::request_data_on_sim_object")] - pub fn request_data_on_sim_object( - &self, - request_id: u32, - period: PeriodEnum, - condition: ConditionEnum, - ) -> Result<(), i32> { - unsafe { - let (simconnect_period, simconnect_interval) = match period { - PeriodEnum::VisualFrame { interval } => ( - bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_VISUAL_FRAME, - interval, - ), - PeriodEnum::Second => (bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_SECOND, 0), - }; - - let simconnect_flags: u32 = match condition { - ConditionEnum::None => 0, - ConditionEnum::Changed => bindings::SIMCONNECT_DATA_REQUEST_FLAG_CHANGED, - }; - - success!(bindings::SimConnect_RequestDataOnSimObject( - self.handle.as_ptr(), - request_id, - request_id, - request_id, - simconnect_period, - simconnect_flags, - 0, - simconnect_interval, - 0, - )); - } - - Ok(()) - } - - #[tracing::instrument(name = "SimConnect::subscribe_to_airport_list")] - pub fn subscribe_to_airport_list(&self, request_id: u32) -> Result<(), i32> { - unsafe { - success!(bindings::SimConnect_SubscribeToFacilities( - self.handle.as_ptr(), - bindings::SIMCONNECT_FACILITY_LIST_TYPE_SIMCONNECT_FACILITY_LIST_TYPE_AIRPORT, - request_id, - )); - } - - Ok(()) - } - - #[tracing::instrument(name = "SimConnect::request_airport_list")] - pub fn request_airport_list(&self, request_id: u32) -> Result<(), i32> { - unsafe { - success!(bindings::SimConnect_RequestFacilitiesList( - self.handle.as_ptr(), - bindings::SIMCONNECT_FACILITY_LIST_TYPE_SIMCONNECT_FACILITY_LIST_TYPE_AIRPORT, - request_id, - )); - } - - Ok(()) - } - - pub fn get_next_dispatch(&self) -> Result, i32> { - let mut data_buf: *mut bindings::SIMCONNECT_RECV = std::ptr::null_mut(); - let mut size_buf: bindings::DWORD = 32; - let size_buf_pointer: *mut bindings::DWORD = &mut size_buf; - - unsafe { - ok_if_fail!( - bindings::SimConnect_GetNextDispatch( - self.handle.as_ptr(), - &mut data_buf, - size_buf_pointer - ), - None - ); - }; - - let result = match unsafe { (*data_buf).dwID as i32 } { - bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_OPEN => Some(Notification::Open), - bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_QUIT => Some(Notification::Quit), - bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_EVENT => { - let event = unsafe { *(data_buf as *const bindings::SIMCONNECT_RECV_EVENT) }; - let event = Event::try_from(event.uEventID).expect("Unrecognized event"); - Some(Notification::Event(event)) - } - bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_SIMOBJECT_DATA => { - let event: &bindings::SIMCONNECT_RECV_SIMOBJECT_DATA = unsafe { - std::mem::transmute_copy( - &(data_buf as *const bindings::SIMCONNECT_RECV_SIMOBJECT_DATA), - ) - }; - - let data_addr = std::ptr::addr_of!(event.dwData); - - Some(Notification::Data(event.dwDefineID, data_addr)) - } - bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_AIRPORT_LIST => { - let event: &bindings::SIMCONNECT_RECV_AIRPORT_LIST = unsafe { - std::mem::transmute_copy( - &(data_buf as *const bindings::SIMCONNECT_RECV_AIRPORT_LIST), - ) - }; - - let data = event - .rgData - .iter() - .map(|data| AirportData { - icao: fixed_c_str_to_string(&data.Icao), - lat: data.Latitude, - lon: data.Longitude, - alt: data.Altitude, - }) - .collect::>(); - - Some(Notification::AirportList(data)) - } - bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_EXCEPTION => { - let event = unsafe { *(data_buf as *const bindings::SIMCONNECT_RECV_EXCEPTION) }; - Some(Notification::Exception(event.dwException)) - } - bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_NULL => None, - _ => panic!("Got unrecognized notification: {}", unsafe { - (*data_buf).dwID as i32 - }), - }; - - Ok(result) - } -} - -pub enum Notification { - Open, - Event(Event), - Data(u32, *const u32), - AirportList(Vec), - Quit, - Exception(u32), -} - -#[derive(Debug, Clone)] -#[allow(dead_code)] -pub struct AirportData { - icao: String, - lat: f64, - lon: f64, - alt: f64, -} - -#[derive(Debug, Copy, Clone, num_enum::TryFromPrimitive)] -#[repr(u32)] -pub enum Event { - Brakes, - BrakesLeft, - AxisLeftBrakeSet, -} - -#[derive(Debug, Clone)] -pub enum PeriodEnum { - VisualFrame { interval: u32 }, - Second, -} - -#[derive(Debug, Clone)] -pub enum ConditionEnum { - None, - Changed, -} - -use std::os::raw::c_char; -impl Event { - fn into_c_char(self) -> *const c_char { - match self { - Event::Brakes => "BRAKES\0".as_ptr() as *const c_char, - Event::BrakesLeft => "BRAKES_LEFT\0".as_ptr() as *const c_char, - Event::AxisLeftBrakeSet => "AXIS_LEFT_BRAKE_SET\0".as_ptr() as *const c_char, - } - } -} - -#[derive(Copy, Clone)] -#[repr(u32)] -enum Group { - Group0, -} - -impl Drop for SimConnect { - fn drop(&mut self) { - let _ = unsafe { bindings::SimConnect_Close(self.handle.as_ptr()) }; - } -} +pub use domain::*; +pub use errors::SimConnectError; +pub use simconnect::SimConnect; +pub use simconnect_object::SimConnectObject; diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..9d0bfa7 --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,29 @@ +macro_rules! success { + ($hr:expr) => {{ + let hr = $hr; + if hr != 0 { + return Err(SimConnectError::SimConnectError(hr)); + } + }}; +} +pub(crate) use success; + +macro_rules! ok_if_fail { + ($hr:expr, $ret:expr) => {{ + let hr = $hr; + let ret = $ret; + if hr != 0 { + return Ok(ret); + } + }}; +} +pub(crate) use ok_if_fail; + +macro_rules! as_c_string { + ($target:expr) => { + std::ffi::CString::new($target) + .map_err(|_| SimConnectError::UnexpectedError("failed to create CString".to_string()))? + .as_ptr() + }; +} +pub(crate) use as_c_string; diff --git a/src/simconnect.rs b/src/simconnect.rs new file mode 100644 index 0000000..2302b77 --- /dev/null +++ b/src/simconnect.rs @@ -0,0 +1,267 @@ +use std::ffi::c_void; + +use crate::{ + as_c_string, bindings, helpers::fixed_c_str_to_string, ok_if_fail, success, AirportData, + ConditionEnum, DataType, Event, Group, Notification, NotificationData, PeriodEnum, + SimConnectError, SimConnectObject, +}; + +#[derive(Debug)] +pub struct SimConnect { + pub handle: std::ptr::NonNull, + pub registered_objects: Vec, +} + +impl SimConnect { + #[tracing::instrument(name = "SimConnect::new")] + pub fn new(name: &str) -> Result { + let mut handle = std::ptr::null_mut(); + + success!(unsafe { + bindings::SimConnect_Open( + &mut handle, + as_c_string!(name), + std::ptr::null_mut(), + 0, + std::ptr::null_mut(), + 0, + ) + }); + + Ok(Self { + handle: std::ptr::NonNull::new(handle).ok_or_else(|| { + SimConnectError::UnexpectedError( + "SimConnect_Open returned null pointer on success".to_string(), + ) + })?, + registered_objects: Vec::new(), + }) + } + + pub fn register_object(&mut self) -> Result { + let type_id: String = std::any::type_name::().into(); + + if self.registered_objects.contains(&type_id) { + return Err(SimConnectError::ObjectAlreadyRegistered(type_id)); + } + + self.registered_objects.push(type_id.clone()); + + let id = self + .registered_objects + .iter() + .position(|p| p == &type_id) + .ok_or_else(|| { + SimConnectError::UnexpectedError("failed to find registered event".to_string()) + })?; + let id = u32::try_from(id)?; + + T::register(self, id)?; + + Ok(id) + } + + #[tracing::instrument(name = "SimConnect::register_event")] + pub fn register_event(&self, event: Event) -> Result<(), SimConnectError> { + success!(unsafe { + bindings::SimConnect_MapClientEventToSimEvent( + self.handle.as_ptr(), + event as u32, + event.into_c_char(), + ) + }); + + let group = Group::Group0; + + success!(unsafe { + bindings::SimConnect_AddClientEventToNotificationGroup( + self.handle.as_ptr(), + group as u32, + event as u32, + 0, + ) + }); + + success!(unsafe { + bindings::SimConnect_SetNotificationGroupPriority(self.handle.as_ptr(), group as u32, 1) + }); + + Ok(()) + } + + #[tracing::instrument(name = "SimConnect::add_to_data_definition")] + pub fn add_to_data_definition( + &self, + request_id: u32, + datum_name: &str, + units_name: &str, + data_type: DataType, + ) -> Result<(), SimConnectError> { + let c_type = match data_type { + DataType::F64 => bindings::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_FLOAT64, + DataType::Bool => bindings::SIMCONNECT_DATATYPE_SIMCONNECT_DATATYPE_INT32, + }; + + unsafe { + success!(bindings::SimConnect_AddToDataDefinition( + self.handle.as_ptr(), + request_id, + as_c_string!(datum_name), + as_c_string!(units_name), + c_type, + 0.0, + u32::MAX, + )); + } + + Ok(()) + } + + #[tracing::instrument(name = "SimConnect::request_data_on_sim_object")] + pub fn request_data_on_sim_object( + &self, + request_id: u32, + period: PeriodEnum, + condition: ConditionEnum, + ) -> Result<(), SimConnectError> { + unsafe { + let (simconnect_period, simconnect_interval) = match period { + PeriodEnum::VisualFrame { interval } => ( + bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_VISUAL_FRAME, + interval, + ), + PeriodEnum::Second => (bindings::SIMCONNECT_PERIOD_SIMCONNECT_PERIOD_SECOND, 0), + }; + + let simconnect_flags: u32 = match condition { + ConditionEnum::None => 0, + ConditionEnum::Changed => bindings::SIMCONNECT_DATA_REQUEST_FLAG_CHANGED, + }; + + success!(bindings::SimConnect_RequestDataOnSimObject( + self.handle.as_ptr(), + request_id, + request_id, + request_id, + simconnect_period, + simconnect_flags, + 0, + simconnect_interval, + 0, + )); + } + + Ok(()) + } + + #[tracing::instrument(name = "SimConnect::subscribe_to_airport_list")] + pub fn subscribe_to_airport_list(&self, request_id: u32) -> Result<(), SimConnectError> { + unsafe { + success!(bindings::SimConnect_SubscribeToFacilities( + self.handle.as_ptr(), + bindings::SIMCONNECT_FACILITY_LIST_TYPE_SIMCONNECT_FACILITY_LIST_TYPE_AIRPORT, + request_id, + )); + } + + Ok(()) + } + + #[tracing::instrument(name = "SimConnect::request_airport_list")] + pub fn request_airport_list(&self, request_id: u32) -> Result<(), SimConnectError> { + unsafe { + success!(bindings::SimConnect_RequestFacilitiesList( + self.handle.as_ptr(), + bindings::SIMCONNECT_FACILITY_LIST_TYPE_SIMCONNECT_FACILITY_LIST_TYPE_AIRPORT, + request_id, + )); + } + + Ok(()) + } + + pub fn get_next_dispatch(&self) -> Result, SimConnectError> { + let mut data_buf: *mut bindings::SIMCONNECT_RECV = std::ptr::null_mut(); + let mut size_buf: bindings::DWORD = 32; + let size_buf_pointer: *mut bindings::DWORD = &mut size_buf; + + unsafe { + ok_if_fail!( + bindings::SimConnect_GetNextDispatch( + self.handle.as_ptr(), + &mut data_buf, + size_buf_pointer + ), + None + ); + }; + + let result = match unsafe { (*data_buf).dwID as i32 } { + bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_OPEN => Some(Notification::Open), + bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_QUIT => Some(Notification::Quit), + bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_EVENT => { + let event = unsafe { *(data_buf as *const bindings::SIMCONNECT_RECV_EVENT) }; + let event = Event::try_from(event.uEventID) + .map_err(|_| SimConnectError::SimConnectUnrecognizedEvent(event.uEventID))?; + Some(Notification::Event(event)) + } + bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_SIMOBJECT_DATA => { + let event: &bindings::SIMCONNECT_RECV_SIMOBJECT_DATA = unsafe { + std::mem::transmute_copy( + &(data_buf as *const bindings::SIMCONNECT_RECV_SIMOBJECT_DATA), + ) + }; + + let object_type = self.registered_objects.get(event.dwDefineID as usize); + + match object_type { + Some(object_type) => { + let data = NotificationData { + type_id: object_type.clone(), + data_addr: std::ptr::addr_of!(event.dwData), + }; + + Some(Notification::Data(data)) + } + _ => None, + } + } + bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_AIRPORT_LIST => { + let event: &bindings::SIMCONNECT_RECV_AIRPORT_LIST = unsafe { + std::mem::transmute_copy( + &(data_buf as *const bindings::SIMCONNECT_RECV_AIRPORT_LIST), + ) + }; + + let data = event + .rgData + .iter() + .map(|data| AirportData { + icao: fixed_c_str_to_string(&data.Icao), + lat: data.Latitude, + lon: data.Longitude, + alt: data.Altitude, + }) + .collect::>(); + + Some(Notification::AirportList(data)) + } + bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_EXCEPTION => { + let event = unsafe { *(data_buf as *const bindings::SIMCONNECT_RECV_EXCEPTION) }; + Some(Notification::Exception(event.dwException)) + } + bindings::SIMCONNECT_RECV_ID_SIMCONNECT_RECV_ID_NULL => None, + _ => panic!("Got unrecognized notification: {}", unsafe { + (*data_buf).dwID as i32 + }), + }; + + Ok(result) + } +} + +impl Drop for SimConnect { + fn drop(&mut self) { + let _ = unsafe { bindings::SimConnect_Close(self.handle.as_ptr()) }; + } +} diff --git a/src/simconnect_object.rs b/src/simconnect_object.rs new file mode 100644 index 0000000..6990dc3 --- /dev/null +++ b/src/simconnect_object.rs @@ -0,0 +1,5 @@ +use crate::{NotificationData, SimConnect, SimConnectError}; + +pub trait SimConnectObject: Clone + for<'a> TryFrom<&'a NotificationData> { + fn register(client: &mut SimConnect, id: u32) -> Result<(), SimConnectError>; +}