diff --git a/Cargo.lock b/Cargo.lock index 2d852b96a69..985cd4c9575 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,6 +214,18 @@ dependencies = [ "vec_map", ] +[[package]] +name = "enum_dispatch" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd53b3fde38a39a06b2e66dc282f3e86191e53bd04cc499929c15742beae3df8" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -621,6 +633,12 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" +[[package]] +name = "once_cell" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" + [[package]] name = "paste" version = "1.0.5" @@ -934,6 +952,7 @@ dependencies = [ name = "systems_wasm" version = "0.1.0" dependencies = [ + "enum_dispatch", "fxhash", "msfs", "systems", diff --git a/docs/a320-simvars.md b/docs/a320-simvars.md index ea5c7ca690d..cfa7b048240 100644 --- a/docs/a320-simvars.md +++ b/docs/a320-simvars.md @@ -821,18 +821,13 @@ - Bool - NW STRG DISC memo indication should show on ecam if true -- A32NX_TILLER_PEDAL_DISCONNECT - - Bool - - True when tiller disconnect button is pressed - Tiller button to be binded on "TOGGLE WATER RUDDER" - - A32NX_NOSE_WHEEL_POSITION - Percent over 100 - Position of nose steering wheel animation [0;1] 0 left, 0.5 middle - A32NX_TILLER_HANDLE_POSITION - Percent over 100 - - Position of tiller steering handle animation [0;1] 0 left, 0.5 middle + - Position of tiller steering handle animation [-1;1] -1 left, 0 middle, 1 right - A32NX_AUTOPILOT_NOSEWHEEL_DEMAND - Percent over 100 diff --git a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml index cd780824c68..e8e88e31851 100644 --- a/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml +++ b/flybywire-aircraft-a320-neo/SimObjects/AirPlanes/FlyByWire_A320_NEO/model/A320_NEO_INTERIOR.xml @@ -461,7 +461,8 @@ HANDLE_LEFT_YOKE HANDLE_LEFT_YOKE - (L:A32NX_TILLER_HANDLE_POSITION) + + (L:A32NX_TILLER_HANDLE_POSITION) 1 + 2 / 1 diff --git a/src/systems/a320_systems/src/hydraulic/mod.rs b/src/systems/a320_systems/src/hydraulic/mod.rs index 2d9d3507f46..0dbaee5849c 100644 --- a/src/systems/a320_systems/src/hydraulic/mod.rs +++ b/src/systems/a320_systems/src/hydraulic/mod.rs @@ -1702,7 +1702,7 @@ impl A320HydraulicBrakeSteerComputerUnit { .get_identifier("RIGHT_BRAKE_PEDAL_INPUT".to_owned()), ground_speed_id: context.get_identifier("GPS GROUND SPEED".to_owned()), - rudder_pedal_input_id: context.get_identifier("RUDDER_PEDAL_POSITION".to_owned()), + rudder_pedal_input_id: context.get_identifier("RUDDER_PEDAL_POSITION_RATIO".to_owned()), tiller_handle_input_id: context.get_identifier("TILLER_HANDLE_POSITION".to_owned()), tiller_pedal_disconnect_id: context .get_identifier("TILLER_PEDAL_DISCONNECT".to_owned()), @@ -1963,9 +1963,9 @@ impl SimulationElement for A320HydraulicBrakeSteerComputerUnit { self.is_gear_lever_down = reader.read(&self.gear_handle_position_id); self.anti_skid_activated = reader.read(&self.antiskid_brakes_active_id); self.left_brake_pilot_input = - Ratio::new::(reader.read(&self.left_brake_pedal_input_id)); + Ratio::new::(reader.read(&self.left_brake_pedal_input_id)); self.right_brake_pilot_input = - Ratio::new::(reader.read(&self.right_brake_pedal_input_id)); + Ratio::new::(reader.read(&self.right_brake_pedal_input_id)); self.tiller_handle_position = Ratio::new::(reader.read(&self.tiller_handle_input_id)); @@ -3567,17 +3567,13 @@ mod tests { .air_press_nominal() } - fn set_left_brake(self, position_percent: Ratio) -> Self { - self.set_brake("LEFT_BRAKE_PEDAL_INPUT", position_percent) - } - - fn set_right_brake(self, position_percent: Ratio) -> Self { - self.set_brake("RIGHT_BRAKE_PEDAL_INPUT", position_percent) + fn set_left_brake(mut self, position: Ratio) -> Self { + self.write_by_name("LEFT_BRAKE_PEDAL_INPUT", position); + self } - fn set_brake(mut self, name: &str, position_percent: Ratio) -> Self { - let scaled_value = position_percent.get::(); - self.write_by_name(name, scaled_value.min(1.).max(0.)); + fn set_right_brake(mut self, position: Ratio) -> Self { + self.write_by_name("RIGHT_BRAKE_PEDAL_INPUT", position); self } diff --git a/src/systems/a320_systems_wasm/src/autobrakes.rs b/src/systems/a320_systems_wasm/src/autobrakes.rs new file mode 100644 index 00000000000..c0f1e56ac15 --- /dev/null +++ b/src/systems/a320_systems_wasm/src/autobrakes.rs @@ -0,0 +1,39 @@ +use std::error::Error; +use std::time::Duration; +use systems_wasm::aspects::{EventToVariableMapping, EventToVariableOptions, MsfsAspectBuilder}; +use systems_wasm::Variable; + +pub(super) fn autobrakes(builder: &mut MsfsAspectBuilder) -> Result<(), Box> { + let options = |options: EventToVariableOptions| { + options + .leading_debounce(Duration::from_millis(1500)) + .afterwards_reset_to(0.) + }; + + builder.event_to_variable( + "AUTOBRAKE_LO_SET", + EventToVariableMapping::Value(1.), + Variable::named("OVHD_AUTOBRK_LOW_ON_IS_PRESSED"), + options, + )?; + builder.event_to_variable( + "AUTOBRAKE_MED_SET", + EventToVariableMapping::Value(1.), + Variable::named("OVHD_AUTOBRK_MED_ON_IS_PRESSED"), + options, + )?; + builder.event_to_variable( + "AUTOBRAKE_HI_SET", + EventToVariableMapping::Value(1.), + Variable::named("OVHD_AUTOBRK_MAX_ON_IS_PRESSED"), + options, + )?; + builder.event_to_variable( + "AUTOBRAKE_DISARM", + EventToVariableMapping::Value(1.), + Variable::named("AUTOBRAKE_DISARM"), + |options| options.afterwards_reset_to(0.), + )?; + + Ok(()) +} diff --git a/src/systems/a320_systems_wasm/src/brakes.rs b/src/systems/a320_systems_wasm/src/brakes.rs new file mode 100644 index 00000000000..3b220e8665f --- /dev/null +++ b/src/systems/a320_systems_wasm/src/brakes.rs @@ -0,0 +1,107 @@ +use std::error::Error; +use systems::shared::{from_bool, to_bool}; +use systems_wasm::aspects::{ + max, EventToVariableMapping, ExecuteOn, MsfsAspectBuilder, VariableToEventMapping, + VariableToEventWriteOn, +}; +use systems_wasm::Variable; + +pub(super) fn brakes(builder: &mut MsfsAspectBuilder) -> Result<(), Box> { + builder.event_to_variable( + "PARKING_BRAKES", + EventToVariableMapping::CurrentValueToValue(|current_value| { + from_bool(!to_bool(current_value)) + }), + Variable::named("PARK_BRAKE_LEVER_POS"), + |options| options.mask(), + )?; + builder.event_to_variable( + "PARKING_BRAKE_SET", + EventToVariableMapping::EventDataToValue(|event_data| from_bool(event_data == 1)), + Variable::named("PARK_BRAKE_LEVER_POS"), + |options| options.mask(), + )?; + + // Controller inputs for the left and right brakes are captured and translated + // to a variable so that it can be used by the simulation. + // After running the simulation, the variable value is written back to the simulator + // through the event. + let axis_left_brake_set_event_id = builder.event_to_variable( + "AXIS_LEFT_BRAKE_SET", + EventToVariableMapping::EventData32kPosition, + Variable::aspect("BRAKES_LEFT_EVENT"), + |options| options.mask(), + )?; + builder.variable_to_event_id( + Variable::aspect("BRAKE LEFT FORCE FACTOR"), + VariableToEventMapping::EventData32kPosition, + VariableToEventWriteOn::EveryTick, + axis_left_brake_set_event_id, + ); + let axis_right_brake_set_event_id = builder.event_to_variable( + "AXIS_RIGHT_BRAKE_SET", + EventToVariableMapping::EventData32kPosition, + Variable::aspect("BRAKES_RIGHT_EVENT"), + |options| options.mask(), + )?; + builder.variable_to_event_id( + Variable::aspect("BRAKE RIGHT FORCE FACTOR"), + VariableToEventMapping::EventData32kPosition, + VariableToEventWriteOn::EveryTick, + axis_right_brake_set_event_id, + ); + + // Inputs for both brakes, left brake, and right brake are captured and + // translated via a smooth press function into a ratio which is written to variables. + const KEYBOARD_PRESS_SPEED: f64 = 0.6; + const KEYBOARD_RELEASE_SPEED: f64 = 0.3; + builder.event_to_variable( + "BRAKES", + EventToVariableMapping::SmoothPress(KEYBOARD_PRESS_SPEED, KEYBOARD_RELEASE_SPEED), + Variable::aspect("BRAKES"), + |options| options.mask(), + )?; + builder.event_to_variable( + "BRAKES_LEFT", + EventToVariableMapping::SmoothPress(KEYBOARD_PRESS_SPEED, KEYBOARD_RELEASE_SPEED), + Variable::aspect("BRAKES_LEFT"), + |options| options.mask(), + )?; + builder.event_to_variable( + "BRAKES_RIGHT", + EventToVariableMapping::SmoothPress(KEYBOARD_PRESS_SPEED, KEYBOARD_RELEASE_SPEED), + Variable::aspect("BRAKES_RIGHT"), + |options| options.mask(), + )?; + + // The maximum braking demand of all controller inputs + // is calculated and made available as a percentage. + builder.reduce( + ExecuteOn::PreTick, + vec![ + Variable::aspect("BRAKES"), + Variable::aspect("BRAKES_LEFT"), + Variable::aspect("BRAKES_LEFT_EVENT"), + ], + 0., + to_percent_max, + Variable::named("LEFT_BRAKE_PEDAL_INPUT"), + ); + builder.reduce( + ExecuteOn::PreTick, + vec![ + Variable::aspect("BRAKES"), + Variable::aspect("BRAKES_RIGHT"), + Variable::aspect("BRAKES_RIGHT_EVENT"), + ], + 0., + to_percent_max, + Variable::named("RIGHT_BRAKE_PEDAL_INPUT"), + ); + + Ok(()) +} + +fn to_percent_max(accumulator: f64, item: f64) -> f64 { + max(accumulator, item * 100.) +} diff --git a/src/systems/a320_systems_wasm/src/flaps.rs b/src/systems/a320_systems_wasm/src/flaps.rs new file mode 100644 index 00000000000..a037fa24f94 --- /dev/null +++ b/src/systems/a320_systems_wasm/src/flaps.rs @@ -0,0 +1,206 @@ +use msfs::sim_connect; +use msfs::{sim_connect::SimConnect, sim_connect::SIMCONNECT_OBJECT_ID_USER}; +use std::error::Error; +use systems_wasm::aspects::{ + EventToVariableMapping, ExecuteOn, MsfsAspectBuilder, VariablesToObject, +}; +use systems_wasm::{set_data_on_sim_object, Variable}; + +pub(super) fn flaps(builder: &mut MsfsAspectBuilder) -> Result<(), Box> { + builder.event_to_variable( + "FLAPS_INCR", + EventToVariableMapping::CurrentValueToValue(|current_value| (current_value + 1.).min(4.)), + Variable::named("FLAPS_HANDLE_INDEX"), + |options| options.mask(), + )?; + builder.event_to_variable( + "FLAPS_DECR", + EventToVariableMapping::CurrentValueToValue(|current_value| (current_value - 1.).max(0.)), + Variable::named("FLAPS_HANDLE_INDEX"), + |options| options.mask(), + )?; + flaps_event_to_value(builder, "FLAPS_UP", 0.)?; + flaps_event_to_value(builder, "FLAPS_1", 1.)?; + flaps_event_to_value(builder, "FLAPS_2", 2.)?; + flaps_event_to_value(builder, "FLAPS_3", 3.)?; + flaps_event_to_value(builder, "FLAPS_DOWN", 4.)?; + builder.event_to_variable( + "FLAPS_SET", + EventToVariableMapping::EventDataAndCurrentValueToValue(|event_data, current_value| { + let normalized_input: f64 = (event_data as i32 as f64) / 8192. - 1.; + get_handle_pos_from_0_1(normalized_input, current_value) + }), + Variable::named("FLAPS_HANDLE_INDEX"), + |options| options.mask(), + )?; + builder.event_to_variable( + "AXIS_FLAPS_SET", + EventToVariableMapping::EventDataAndCurrentValueToValue(|event_data, current_value| { + let normalized_input: f64 = (event_data as i32 as f64) / 16384.; + get_handle_pos_from_0_1(normalized_input, current_value) + }), + Variable::named("FLAPS_HANDLE_INDEX"), + |options| options.mask(), + )?; + + builder.map( + ExecuteOn::PreTick, + Variable::named("FLAPS_HANDLE_INDEX"), + |value| value / 4., + Variable::named("FLAPS_HANDLE_PERCENT"), + ); + + builder.variables_to_object(Box::new(FlapsSurface { + left_flap: 0., + right_flap: 0., + })); + builder.variables_to_object(Box::new(SlatsSurface { + left_slat: 0., + right_slat: 0., + })); + builder.variables_to_object(Box::new(FlapsHandleIndex { index: 0. })); + + Ok(()) +} + +fn flaps_event_to_value( + builder: &mut MsfsAspectBuilder, + event_name: &str, + value: f64, +) -> Result<(), Box> { + builder.event_to_variable( + event_name, + EventToVariableMapping::Value(value), + Variable::named("FLAPS_HANDLE_INDEX"), + |options| options.mask(), + )?; + + Ok(()) +} + +fn get_handle_pos_from_0_1(input: f64, current_value: f64) -> f64 { + if input < -0.8 { + 0. + } else if input > -0.7 && input < -0.3 { + 1. + } else if input > -0.2 && input < 0.2 { + 2. + } else if input > 0.3 && input < 0.7 { + 3. + } else if input > 0.8 { + 4. + } else { + current_value + } +} + +#[sim_connect::data_definition] +struct FlapsSurface { + #[name = "TRAILING EDGE FLAPS LEFT PERCENT"] + #[unit = "Percent"] + left_flap: f64, + + #[name = "TRAILING EDGE FLAPS RIGHT PERCENT"] + #[unit = "Percent"] + right_flap: f64, +} + +impl VariablesToObject for FlapsSurface { + fn variables(&self) -> Vec { + vec![ + Variable::named("LEFT_FLAPS_POSITION_PERCENT"), + Variable::named("RIGHT_FLAPS_POSITION_PERCENT"), + ] + } + + fn write(&mut self, values: Vec) { + self.left_flap = values[0]; + self.right_flap = values[1]; + } + + set_data_on_sim_object!(); +} + +#[sim_connect::data_definition] +struct SlatsSurface { + #[name = "LEADING EDGE FLAPS LEFT PERCENT"] + #[unit = "Percent"] + left_slat: f64, + + #[name = "LEADING EDGE FLAPS RIGHT PERCENT"] + #[unit = "Percent"] + right_slat: f64, +} + +impl VariablesToObject for SlatsSurface { + fn variables(&self) -> Vec { + vec![ + Variable::named("LEFT_SLATS_POSITION_PERCENT"), + Variable::named("RIGHT_SLATS_POSITION_PERCENT"), + ] + } + + fn write(&mut self, values: Vec) { + self.left_slat = values[0]; + self.right_slat = values[1]; + } + + set_data_on_sim_object!(); +} + +#[sim_connect::data_definition] +struct FlapsHandleIndex { + #[name = "FLAPS HANDLE INDEX"] + #[unit = "Number"] + index: f64, +} + +impl VariablesToObject for FlapsHandleIndex { + fn variables(&self) -> Vec { + vec![ + Variable::named("LEFT_FLAPS_POSITION_PERCENT"), + Variable::named("RIGHT_FLAPS_POSITION_PERCENT"), + Variable::named("LEFT_SLATS_POSITION_PERCENT"), + Variable::named("RIGHT_SLATS_POSITION_PERCENT"), + ] + } + + fn write(&mut self, values: Vec) { + self.index = Self::msfs_flap_index_from_surfaces_positions_percent(values); + } + + set_data_on_sim_object!(); +} + +impl FlapsHandleIndex { + /// Tries to take actual surfaces position PERCENTS and convert it into flight model FLAP HANDLE INDEX + /// This index is used by MSFS to select correct aerodynamic properties + /// There is no index available for flaps but no slats configurations (possible plane failure case) + /// The percent thresholds can be tuned to change the timing of aerodynamic impact versus surface actual position + fn msfs_flap_index_from_surfaces_positions_percent(values: Vec) -> f64 { + let left_flaps_position = values[0]; + let right_flaps_position = values[1]; + let left_slats_position = values[2]; + let right_slats_position = values[3]; + let flap_mean_position = (left_flaps_position + right_flaps_position) / 2.; + let slat_mean_position = (left_slats_position + right_slats_position) / 2.; + + if flap_mean_position < 2. && slat_mean_position < 2. { + // Clean configuration no flaps no slats + 0. + } else if flap_mean_position < 12. && slat_mean_position > 15. { + // Almost no flaps but some slats -> CONF 1 + 1. + } else if flap_mean_position > 80. { + 5. + } else if flap_mean_position > 49. { + 4. + } else if flap_mean_position > 30. { + 3. + } else if flap_mean_position > 12. { + 2. + } else { + 0. + } + } +} diff --git a/src/systems/a320_systems_wasm/src/lib.rs b/src/systems/a320_systems_wasm/src/lib.rs index 26a2dae383b..537e5982fa8 100644 --- a/src/systems/a320_systems_wasm/src/lib.rs +++ b/src/systems/a320_systems_wasm/src/lib.rs @@ -1,36 +1,26 @@ #![cfg(any(target_arch = "wasm32", doc))] -use std::{ - error::Error, - time::{Duration, Instant}, -}; +mod autobrakes; +mod brakes; +mod flaps; +mod nose_wheel_steering; use a320_systems::A320; -use msfs::sim_connect; -use msfs::{ - legacy::{AircraftVariable, NamedVariable}, - sim_connect::SimConnect, - sim_connect::{SimConnectRecv, SIMCONNECT_OBJECT_ID_USER}, - sys, -}; - -use systems::{ - failures::FailureType, - shared::HydraulicColor, - simulation::{VariableIdentifier, VariableRegistry}, -}; -use systems_wasm::{ - f64_to_sim_connect_32k_pos, sim_connect_32k_pos_to_f64, MsfsAspectCtor, MsfsSimulationBuilder, - MsfsVariableRegistry, SimulatorAspect, -}; +use autobrakes::autobrakes; +use brakes::brakes; +use flaps::flaps; +use nose_wheel_steering::nose_wheel_steering; +use std::error::Error; +use systems::{failures::FailureType, shared::HydraulicColor}; +use systems_wasm::aspects::ExecuteOn; +use systems_wasm::{MsfsSimulationBuilder, Variable}; #[msfs::gauge(name=systems)] async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { let mut sim_connect = gauge.open_simconnect("systems")?; let (mut simulation, mut handler) = - MsfsSimulationBuilder::new("A32NX_".to_owned(), sim_connect.as_mut().get_mut()) + MsfsSimulationBuilder::new("A32NX_", sim_connect.as_mut().get_mut()) .with_electrical_buses(vec![ - ("AC_1", 2), ("AC_1", 2), ("AC_2", 3), ("AC_ESS", 4), @@ -46,12 +36,7 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { ("DC_HOT_2", 13), ("DC_GND_FLT_SVC", 15), ]) - .with_auxiliary_power_unit("OVHD_APU_START_PB_IS_AVAILABLE".to_owned(), 8)? - .with::()? - .with::()? - .with::()? - .with::()? - .with::()? + .with_auxiliary_power_unit("OVHD_APU_START_PB_IS_AVAILABLE", 8)? .with_failures(vec![ (24_000, FailureType::TransformerRectifier(1)), (24_001, FailureType::TransformerRectifier(2)), @@ -89,36 +74,7 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { .provides_aircraft_variable("AMBIENT WIND DIRECTION", "Degrees", 0)? .provides_aircraft_variable("AMBIENT WIND VELOCITY", "Knots", 0)? .provides_aircraft_variable("ANTISKID BRAKES ACTIVE", "Bool", 0)? - .provides_aircraft_variable_with_additional_names( - "APU GENERATOR SWITCH", - "Bool", - 0, - vec!["OVHD_ELEC_APU_GEN_PB_IS_ON".to_owned()], - )? - .provides_aircraft_variable_with_additional_names( - "BLEED AIR ENGINE", - "Bool", - 1, - vec!["OVHD_PNEU_ENG_1_BLEED_PB_IS_AUTO".to_owned()], - )? - .provides_aircraft_variable_with_additional_names( - "BLEED AIR ENGINE", - "Bool", - 2, - vec!["OVHD_PNEU_ENG_2_BLEED_PB_IS_AUTO".to_owned()], - )? - .provides_aircraft_variable_with_additional_names( - "EXTERNAL POWER AVAILABLE", - "Bool", - 1, - vec!["OVHD_ELEC_EXT_PWR_PB_IS_AVAILABLE".to_owned()], - )? - .provides_aircraft_variable_with_additional_names( - "EXTERNAL POWER ON", - "Bool", - 1, - vec!["OVHD_ELEC_EXT_PWR_PB_IS_ON".to_owned()], - )? + .provides_aircraft_variable("EXTERNAL POWER AVAILABLE", "Bool", 1)? .provides_aircraft_variable("FUEL TANK LEFT MAIN QUANTITY", "Pounds", 0)? .provides_aircraft_variable("GEAR ANIMATION POSITION", "Percent", 0)? .provides_aircraft_variable("GEAR ANIMATION POSITION", "Percent", 1)? @@ -127,18 +83,6 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { .provides_aircraft_variable("GEAR LEFT POSITION", "Percent", 0)? .provides_aircraft_variable("GEAR RIGHT POSITION", "Percent", 0)? .provides_aircraft_variable("GEAR HANDLE POSITION", "Bool", 0)? - .provides_aircraft_variable_with_additional_names( - "GENERAL ENG MASTER ALTERNATOR", - "Bool", - 1, - vec!["OVHD_ELEC_ENG_GEN_1_PB_IS_ON".to_owned()], - )? - .provides_aircraft_variable_with_additional_names( - "GENERAL ENG MASTER ALTERNATOR", - "Bool", - 2, - vec!["OVHD_ELEC_ENG_GEN_2_PB_IS_ON".to_owned()], - )? .provides_aircraft_variable("GENERAL ENG STARTER ACTIVE", "Bool", 1)? .provides_aircraft_variable("GENERAL ENG STARTER ACTIVE", "Bool", 2)? .provides_aircraft_variable("GPS GROUND SPEED", "Knots", 0)? @@ -162,6 +106,52 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { .provides_aircraft_variable("TURB ENG CORRECTED N2", "Percent", 2)? .provides_aircraft_variable("UNLIMITED FUEL", "Bool", 0)? .provides_aircraft_variable("VELOCITY WORLD Y", "feet per minute", 0)? + .with_aspect(|builder| { + builder.copy( + Variable::aircraft("APU GENERATOR SWITCH", "Bool", 0), + Variable::aspect("OVHD_ELEC_APU_GEN_PB_IS_ON"), + ); + + builder.copy( + Variable::aircraft("BLEED AIR ENGINE", "Bool", 1), + Variable::aspect("OVHD_PNEU_ENG_1_BLEED_PB_IS_AUTO"), + ); + builder.copy( + Variable::aircraft("BLEED AIR ENGINE", "Bool", 2), + Variable::aspect("OVHD_PNEU_ENG_2_BLEED_PB_IS_AUTO"), + ); + + builder.copy( + Variable::aircraft("EXTERNAL POWER AVAILABLE", "Bool", 1), + Variable::aspect("OVHD_ELEC_EXT_PWR_PB_IS_AVAILABLE"), + ); + builder.copy( + Variable::aircraft("EXTERNAL POWER ON", "Bool", 1), + Variable::aspect("OVHD_ELEC_EXT_PWR_PB_IS_ON"), + ); + + builder.copy( + Variable::aircraft("GENERAL ENG MASTER ALTERNATOR", "Bool", 1), + Variable::aspect("OVHD_ELEC_ENG_GEN_1_PB_IS_ON"), + ); + builder.copy( + Variable::aircraft("GENERAL ENG MASTER ALTERNATOR", "Bool", 2), + Variable::aspect("OVHD_ELEC_ENG_GEN_2_PB_IS_ON"), + ); + + builder.map( + ExecuteOn::PreTick, + Variable::aircraft("INTERACTIVE POINT OPEN", "Position", 5), + |value| if value > 0. { 1. } else { 0. }, + Variable::aspect("FWD_DOOR_CARGO_OPEN_REQ"), + ); + + Ok(()) + })? + .with_aspect(brakes)? + .with_aspect(autobrakes)? + .with_aspect(nose_wheel_steering)? + .with_aspect(flaps)? .build(A320::new)?; while let Some(event) = gauge.next_event().await { @@ -170,1061 +160,3 @@ async fn systems(mut gauge: msfs::Gauge) -> Result<(), Box> { Ok(()) } - -#[sim_connect::data_definition] -struct FlapsSurface { - #[name = "TRAILING EDGE FLAPS LEFT PERCENT"] - #[unit = "Percent"] - left_flap: f64, - - #[name = "TRAILING EDGE FLAPS RIGHT PERCENT"] - #[unit = "Percent"] - right_flap: f64, -} - -#[sim_connect::data_definition] -struct SlatsSurface { - #[name = "LEADING EDGE FLAPS LEFT PERCENT"] - #[unit = "Percent"] - left_slat: f64, - - #[name = "LEADING EDGE FLAPS RIGHT PERCENT"] - #[unit = "Percent"] - right_slat: f64, -} - -#[sim_connect::data_definition] -struct FlapsHandleIndex { - #[name = "FLAPS HANDLE INDEX"] - #[unit = "Number"] - index: f64, -} - -struct Flaps { - flaps_left_position_id: VariableIdentifier, - flaps_right_position_id: VariableIdentifier, - slats_left_position_id: VariableIdentifier, - slats_right_position_id: VariableIdentifier, - - //IDs of the flaps handle events - id_flaps_incr: sys::DWORD, - id_flaps_decr: sys::DWORD, - id_flaps_1: sys::DWORD, - id_flaps_2: sys::DWORD, - id_flaps_3: sys::DWORD, - id_flaps_set: sys::DWORD, - id_axis_flaps_set: sys::DWORD, - id_flaps_down: sys::DWORD, - id_flaps_up: sys::DWORD, - - //LVars to communicate between the flap movement logic - //and the simulation animation - flaps_handle_index_sim_var: NamedVariable, - flaps_handle_percent_sim_var: NamedVariable, - left_flaps_position_sim_var: NamedVariable, - right_flaps_position_sim_var: NamedVariable, - left_slats_position_sim_var: NamedVariable, - right_slats_position_sim_var: NamedVariable, - - msfs_flaps_handle_index: FlapsHandleIndex, - flaps_surface_sim_object: FlapsSurface, - slats_surface_sim_object: SlatsSurface, - flaps_handle_position: u8, - left_flaps_position: f64, - right_flaps_position: f64, - - left_slats_position: f64, - right_slats_position: f64, -} -impl MsfsAspectCtor for Flaps { - fn new( - registry: &mut MsfsVariableRegistry, - sim_connect: &mut SimConnect, - ) -> Result> { - Ok(Self { - flaps_left_position_id: registry.get("LEFT_FLAPS_POSITION_PERCENT".to_owned()), - flaps_right_position_id: registry.get("RIGHT_FLAPS_POSITION_PERCENT".to_owned()), - slats_left_position_id: registry.get("LEFT_SLATS_POSITION_PERCENT".to_owned()), - slats_right_position_id: registry.get("RIGHT_SLATS_POSITION_PERCENT".to_owned()), - - id_flaps_incr: sim_connect.map_client_event_to_sim_event("FLAPS_INCR", true)?, - id_flaps_decr: sim_connect.map_client_event_to_sim_event("FLAPS_DECR", true)?, - id_flaps_1: sim_connect.map_client_event_to_sim_event("FLAPS_1", true)?, - id_flaps_2: sim_connect.map_client_event_to_sim_event("FLAPS_2", true)?, - id_flaps_3: sim_connect.map_client_event_to_sim_event("FLAPS_3", true)?, - id_flaps_set: sim_connect.map_client_event_to_sim_event("FLAPS_SET", true)?, - id_axis_flaps_set: sim_connect.map_client_event_to_sim_event("AXIS_FLAPS_SET", true)?, - id_flaps_down: sim_connect.map_client_event_to_sim_event("FLAPS_DOWN", true)?, - id_flaps_up: sim_connect.map_client_event_to_sim_event("FLAPS_UP", true)?, - - flaps_handle_index_sim_var: NamedVariable::from("A32NX_FLAPS_HANDLE_INDEX"), - flaps_handle_percent_sim_var: NamedVariable::from("A32NX_FLAPS_HANDLE_PERCENT"), - left_flaps_position_sim_var: NamedVariable::from("A32NX_LEFT_FLAPS_POSITION_PERCENT"), - right_flaps_position_sim_var: NamedVariable::from("A32NX_RIGHT_FLAPS_POSITION_PERCENT"), - left_slats_position_sim_var: NamedVariable::from("A32NX_LEFT_SLATS_POSITION_PERCENT"), - right_slats_position_sim_var: NamedVariable::from("A32NX_RIGHT_SLATS_POSITION_PERCENT"), - - msfs_flaps_handle_index: FlapsHandleIndex { index: 0. }, - flaps_surface_sim_object: FlapsSurface { - left_flap: 0., - right_flap: 0., - }, - slats_surface_sim_object: SlatsSurface { - left_slat: 0., - right_slat: 0., - }, - - flaps_handle_position: 0, - left_flaps_position: 0., - right_flaps_position: 0., - - left_slats_position: 0., - right_slats_position: 0., - }) - } -} - -impl Flaps { - fn flaps_handle_position_f64(&self) -> f64 { - self.flaps_handle_position as f64 - } - - /// Tries to take actual surfaces position PERCENTS and convert it into flight model FLAP HANDLE INDEX - /// This index is used by MSFS to select correct aerodynamic properties - /// There is no index available for flaps but no slats configurations (possible plane failure case) - /// The percent thresholds can be tuned to change the timing of aerodynamic impact versus surface actual position - fn msfs_flap_index_from_surfaces_positions_percent(&self) -> u8 { - let flap_mean_position = (self.left_flaps_position + self.right_flaps_position) / 2.; - let slat_mean_position = (self.left_slats_position + self.right_slats_position) / 2.; - - // Clean configuration no flaps no slats - if flap_mean_position < 2. && slat_mean_position < 2. { - return 0; - } - - // Almost no flaps but some slats -> CONF 1 - if flap_mean_position < 12. && slat_mean_position > 15. { - return 1; - } - - if flap_mean_position > 80. { - 5 - } else if flap_mean_position > 49. { - 4 - } else if flap_mean_position > 30. { - 3 - } else if flap_mean_position > 12. { - 2 - } else { - 0 - } - } - - fn get_handle_pos_from_0_1(&self, input: f64) -> u8 { - if input < -0.8 { - 0 - } else if input > -0.7 && input < -0.3 { - 1 - } else if input > -0.2 && input < 0.2 { - 2 - } else if input > 0.3 && input < 0.7 { - 3 - } else if input > 0.8 { - 4 - } else { - self.flaps_handle_position - } - } - - fn get_handle_pos_flaps_set(&self, input: i32) -> u8 { - let normalized_input: f64 = (input as f64) / 8192. - 1.; - return self.get_handle_pos_from_0_1(normalized_input); - } - - fn get_handle_pos_axis_flaps_set(&self, input: i32) -> u8 { - let normalized_input: f64 = (input as f64) / 16384.; - return self.get_handle_pos_from_0_1(normalized_input); - } - - fn catch_event(&mut self, event_id: sys::DWORD, event_data: u32) -> bool { - if event_id == self.id_flaps_incr { - self.flaps_handle_position += 1; - self.flaps_handle_position = self.flaps_handle_position.min(4); - } else if event_id == self.id_flaps_decr { - if self.flaps_handle_position > 0 { - self.flaps_handle_position -= 1; - } - } else if event_id == self.id_flaps_1 { - self.flaps_handle_position = 1; - } else if event_id == self.id_flaps_2 { - self.flaps_handle_position = 2; - } else if event_id == self.id_flaps_3 { - self.flaps_handle_position = 3; - } else if event_id == self.id_flaps_set { - self.flaps_handle_position = self.get_handle_pos_flaps_set(event_data as i32); - } else if event_id == self.id_axis_flaps_set { - self.flaps_handle_position = self.get_handle_pos_axis_flaps_set(event_data as i32); - } else if event_id == self.id_flaps_down { - self.flaps_handle_position = 4; - } else if event_id == self.id_flaps_up { - self.flaps_handle_position = 0; - } else { - return false; - } - self.flaps_handle_index_sim_var - .set_value(self.flaps_handle_position_f64()); - self.flaps_handle_percent_sim_var - .set_value(self.flaps_handle_position_f64() / 4.); - true - } - - fn write_sim_vars(&mut self) { - self.left_flaps_position_sim_var - .set_value(self.left_flaps_position); - self.right_flaps_position_sim_var - .set_value(self.right_flaps_position); - self.left_slats_position_sim_var - .set_value(self.left_slats_position); - self.right_slats_position_sim_var - .set_value(self.right_slats_position); - } -} - -impl SimulatorAspect for Flaps { - fn read(&mut self, identifier: &VariableIdentifier) -> Option { - if identifier == &self.flaps_left_position_id { - Some(self.left_flaps_position) - } else if identifier == &self.flaps_right_position_id { - Some(self.right_flaps_position) - } else if identifier == &self.slats_left_position_id { - Some(self.left_slats_position) - } else if identifier == &self.slats_right_position_id { - Some(self.right_slats_position) - } else { - None - } - } - - fn write(&mut self, identifier: &VariableIdentifier, value: f64) -> bool { - if identifier == &self.flaps_left_position_id { - self.left_flaps_position = value; - true - } else if identifier == &self.flaps_right_position_id { - self.right_flaps_position = value; - true - } else if identifier == &self.slats_left_position_id { - self.left_slats_position = value; - true - } else if identifier == &self.slats_right_position_id { - self.right_slats_position = value; - true - } else { - false - } - } - - fn handle_message(&mut self, message: &SimConnectRecv) -> bool { - if let SimConnectRecv::Event(e) = message { - self.catch_event(e.id(), e.data()) - } else { - false - } - } - - fn post_tick(&mut self, sim_connect: &mut SimConnect) -> Result<(), Box> { - self.flaps_surface_sim_object.left_flap = self.left_flaps_position; - self.flaps_surface_sim_object.right_flap = self.right_flaps_position; - self.slats_surface_sim_object.left_slat = self.left_slats_position; - self.slats_surface_sim_object.right_slat = self.right_slats_position; - self.msfs_flaps_handle_index.index = - self.msfs_flap_index_from_surfaces_positions_percent() as f64; - - self.write_sim_vars(); - - sim_connect - .set_data_on_sim_object(SIMCONNECT_OBJECT_ID_USER, &self.msfs_flaps_handle_index)?; - sim_connect - .set_data_on_sim_object(SIMCONNECT_OBJECT_ID_USER, &self.flaps_surface_sim_object)?; - sim_connect - .set_data_on_sim_object(SIMCONNECT_OBJECT_ID_USER, &self.slats_surface_sim_object)?; - - Ok(()) - } -} - -struct Autobrakes { - autobrake_disarm_id: VariableIdentifier, - ovhd_autobrk_low_on_is_pressed_id: VariableIdentifier, - ovhd_autobrk_med_on_is_pressed_id: VariableIdentifier, - ovhd_autobrk_max_on_is_pressed_id: VariableIdentifier, - - id_mode_max: sys::DWORD, - id_mode_med: sys::DWORD, - id_mode_low: sys::DWORD, - id_disarm: sys::DWORD, - - low_mode_panel_pushbutton: NamedVariable, - med_mode_panel_pushbutton: NamedVariable, - max_mode_panel_pushbutton: NamedVariable, - - low_mode_requested: bool, - med_mode_requested: bool, - max_mode_requested: bool, - disarm_requested: bool, - - last_button_press: Instant, -} - -impl MsfsAspectCtor for Autobrakes { - fn new( - registry: &mut MsfsVariableRegistry, - sim_connect: &mut SimConnect, - ) -> Result> { - Ok(Self { - autobrake_disarm_id: registry.get("AUTOBRAKE_DISARM".to_owned()), - ovhd_autobrk_low_on_is_pressed_id: registry - .get("OVHD_AUTOBRK_LOW_ON_IS_PRESSED".to_owned()), - ovhd_autobrk_med_on_is_pressed_id: registry - .get("OVHD_AUTOBRK_MED_ON_IS_PRESSED".to_owned()), - ovhd_autobrk_max_on_is_pressed_id: registry - .get("OVHD_AUTOBRK_MAX_ON_IS_PRESSED".to_owned()), - - // SimConnect inputs masking - id_mode_max: sim_connect.map_client_event_to_sim_event("AUTOBRAKE_HI_SET", false)?, - id_mode_med: sim_connect.map_client_event_to_sim_event("AUTOBRAKE_MED_SET", false)?, - id_mode_low: sim_connect.map_client_event_to_sim_event("AUTOBRAKE_LO_SET", false)?, - id_disarm: sim_connect.map_client_event_to_sim_event("AUTOBRAKE_DISARM", false)?, - - low_mode_panel_pushbutton: NamedVariable::from("A32NX_OVHD_AUTOBRK_LOW_ON_IS_PRESSED"), - med_mode_panel_pushbutton: NamedVariable::from("A32NX_OVHD_AUTOBRK_MED_ON_IS_PRESSED"), - max_mode_panel_pushbutton: NamedVariable::from("A32NX_OVHD_AUTOBRK_MAX_ON_IS_PRESSED"), - - low_mode_requested: false, - med_mode_requested: false, - max_mode_requested: false, - disarm_requested: false, - - last_button_press: Instant::now(), - }) - } -} - -impl Autobrakes { - // Time to freeze keyboard events once key is released. This will keep key_pressed to TRUE internally when key is actually staying pressed - // but keyboard events wrongly goes to false then back to true for a short period of time due to poor key event handling - const DEFAULT_REARMING_DURATION: Duration = Duration::from_millis(1500); - - fn synchronise_with_sim(&mut self) { - if self.low_mode_panel_pushbutton.get_value() { - self.set_mode_low(); - } - if self.med_mode_panel_pushbutton.get_value() { - self.set_mode_med(); - } - if self.max_mode_panel_pushbutton.get_value() { - self.set_mode_max(); - } - } - - fn reset_events(&mut self) { - if self.last_button_press.elapsed() > Self::DEFAULT_REARMING_DURATION { - self.max_mode_requested = false; - self.med_mode_requested = false; - self.low_mode_requested = false; - } - self.disarm_requested = false; - } - - fn on_receive_pushbutton_event(&mut self) { - self.last_button_press = Instant::now(); - } - - fn set_mode_max(&mut self) { - self.max_mode_requested = true; - self.med_mode_requested = false; - self.low_mode_requested = false; - self.on_receive_pushbutton_event(); - } - - fn set_mode_med(&mut self) { - self.med_mode_requested = true; - self.max_mode_requested = false; - self.low_mode_requested = false; - self.on_receive_pushbutton_event(); - } - - fn set_mode_low(&mut self) { - self.low_mode_requested = true; - self.med_mode_requested = false; - self.max_mode_requested = false; - self.on_receive_pushbutton_event(); - } - - fn set_disarm(&mut self) { - self.disarm_requested = true; - } -} -impl SimulatorAspect for Autobrakes { - fn read(&mut self, identifier: &VariableIdentifier) -> Option { - if identifier == &self.autobrake_disarm_id { - Some(self.disarm_requested as u8 as f64) - } else if identifier == &self.ovhd_autobrk_low_on_is_pressed_id { - Some(self.low_mode_requested as u8 as f64) - } else if identifier == &self.ovhd_autobrk_med_on_is_pressed_id { - Some(self.med_mode_requested as u8 as f64) - } else if identifier == &self.ovhd_autobrk_max_on_is_pressed_id { - Some(self.max_mode_requested as u8 as f64) - } else { - None - } - } - - fn handle_message(&mut self, message: &SimConnectRecv) -> bool { - match message { - SimConnectRecv::Event(e) => { - if e.id() == self.id_mode_low { - self.set_mode_low(); - true - } else if e.id() == self.id_mode_med { - self.set_mode_med(); - true - } else if e.id() == self.id_mode_max { - self.set_mode_max(); - true - } else if e.id() == self.id_disarm { - self.set_disarm(); - true - } else { - false - } - } - _ => false, - } - } - - fn pre_tick(&mut self, _: Duration) { - self.synchronise_with_sim(); - } - - fn post_tick(&mut self, _: &mut SimConnect) -> Result<(), Box> { - self.reset_events(); - - Ok(()) - } -} - -struct Brakes { - park_brak_lever_pos_id: VariableIdentifier, - left_brake_pedal_input_id: VariableIdentifier, - right_brake_pedal_input_id: VariableIdentifier, - brake_left_force_factor_id: VariableIdentifier, - brake_right_force_factor_id: VariableIdentifier, - - park_brake_lever_masked_input: NamedVariable, - left_pedal_brake_masked_input: NamedVariable, - right_pedal_brake_masked_input: NamedVariable, - - id_brake_left: sys::DWORD, - id_brake_right: sys::DWORD, - id_brake_keyboard: sys::DWORD, - id_brake_left_keyboard: sys::DWORD, - id_brake_right_keyboard: sys::DWORD, - id_parking_brake: sys::DWORD, - id_parking_brake_set: sys::DWORD, - - brake_left_sim_input: f64, - brake_right_sim_input: f64, - brake_left_sim_input_keyboard: f64, - brake_right_sim_input_keyboard: f64, - left_key_pressed: bool, - right_key_pressed: bool, - - brake_left_output_to_sim: f64, - brake_right_output_to_sim: f64, - - parking_brake_lever_is_set: bool, - last_transmitted_park_brake_lever_position: f64, -} - -impl MsfsAspectCtor for Brakes { - fn new( - registry: &mut MsfsVariableRegistry, - sim_connect: &mut SimConnect, - ) -> Result> { - Ok(Self { - park_brak_lever_pos_id: registry.get("PARK_BRAKE_LEVER_POS".to_owned()), - left_brake_pedal_input_id: registry.get("LEFT_BRAKE_PEDAL_INPUT".to_owned()), - right_brake_pedal_input_id: registry.get("RIGHT_BRAKE_PEDAL_INPUT".to_owned()), - brake_left_force_factor_id: registry.get("BRAKE LEFT FORCE FACTOR".to_owned()), - brake_right_force_factor_id: registry.get("BRAKE RIGHT FORCE FACTOR".to_owned()), - - park_brake_lever_masked_input: NamedVariable::from("A32NX_PARK_BRAKE_LEVER_POS"), - left_pedal_brake_masked_input: NamedVariable::from("A32NX_LEFT_BRAKE_PEDAL_INPUT"), - right_pedal_brake_masked_input: NamedVariable::from("A32NX_RIGHT_BRAKE_PEDAL_INPUT"), - - // SimConnect inputs masking - id_brake_left: sim_connect - .map_client_event_to_sim_event("AXIS_LEFT_BRAKE_SET", true)?, - id_brake_right: sim_connect - .map_client_event_to_sim_event("AXIS_RIGHT_BRAKE_SET", true)?, - - id_brake_keyboard: sim_connect.map_client_event_to_sim_event("BRAKES", true)?, - id_brake_left_keyboard: sim_connect - .map_client_event_to_sim_event("BRAKES_LEFT", true)?, - id_brake_right_keyboard: sim_connect - .map_client_event_to_sim_event("BRAKES_RIGHT", true)?, - - id_parking_brake: sim_connect.map_client_event_to_sim_event("PARKING_BRAKES", true)?, - id_parking_brake_set: sim_connect - .map_client_event_to_sim_event("PARKING_BRAKE_SET", true)?, - - brake_left_sim_input: 0., - brake_right_sim_input: 0., - brake_left_sim_input_keyboard: 0., - brake_right_sim_input_keyboard: 0., - left_key_pressed: false, - right_key_pressed: false, - - brake_left_output_to_sim: 0., - brake_right_output_to_sim: 0., - - parking_brake_lever_is_set: true, - - last_transmitted_park_brake_lever_position: 1., - }) - } -} - -impl Brakes { - const KEYBOARD_PRESS_SPEED: f64 = 0.6; - const KEYBOARD_RELEASE_SPEED: f64 = 0.3; - - fn set_brake_left(&mut self, simconnect_value: u32) { - self.brake_left_sim_input = sim_connect_32k_pos_to_f64(simconnect_value); - } - - fn set_brake_left_key_pressed(&mut self) { - self.left_key_pressed = true; - } - - fn set_brake_right_key_pressed(&mut self) { - self.right_key_pressed = true; - } - - fn synchronise_with_sim(&mut self) { - // Synchronising WASM park brake state with simulator park brake lever variable - let current_in_sim_park_brake: f64 = self.park_brake_lever_masked_input.get_value(); - - if current_in_sim_park_brake != self.last_transmitted_park_brake_lever_position { - self.receive_a_park_brake_set_event(current_in_sim_park_brake as u32); - } - } - - fn update_keyboard_inputs(&mut self, delta: Duration) { - if self.left_key_pressed { - self.brake_left_sim_input_keyboard += delta.as_secs_f64() * Self::KEYBOARD_PRESS_SPEED; - } else { - self.brake_left_sim_input_keyboard -= - delta.as_secs_f64() * Self::KEYBOARD_RELEASE_SPEED; - } - - if self.right_key_pressed { - self.brake_right_sim_input_keyboard += delta.as_secs_f64() * Self::KEYBOARD_PRESS_SPEED; - } else { - self.brake_right_sim_input_keyboard -= - delta.as_secs_f64() * Self::KEYBOARD_RELEASE_SPEED; - } - - self.brake_right_sim_input_keyboard = self.brake_right_sim_input_keyboard.min(1.).max(0.); - self.brake_left_sim_input_keyboard = self.brake_left_sim_input_keyboard.min(1.).max(0.); - } - - fn reset_keyboard_events(&mut self) { - self.left_key_pressed = false; - self.right_key_pressed = false; - } - - fn transmit_masked_inputs(&mut self) { - let park_is_set = self.parking_brake_lever_is_set as u32 as f64; - self.last_transmitted_park_brake_lever_position = park_is_set; - self.park_brake_lever_masked_input.set_value(park_is_set); - - let brake_right = self.brake_right() * 100.; - let brake_left = self.brake_left() * 100.; - self.right_pedal_brake_masked_input.set_value(brake_right); - self.left_pedal_brake_masked_input.set_value(brake_left); - } - - fn transmit_client_events( - &mut self, - sim_connect: &mut SimConnect, - ) -> Result<(), Box> { - // We want to send our brake commands once per refresh event, thus doing it after a draw event - sim_connect.transmit_client_event( - SIMCONNECT_OBJECT_ID_USER, - self.id_brake_left, - self.get_brake_left_output_converted_in_simconnect_format(), - )?; - - sim_connect.transmit_client_event( - SIMCONNECT_OBJECT_ID_USER, - self.id_brake_right, - self.get_brake_right_output_converted_in_simconnect_format(), - )?; - - Ok(()) - } - - fn set_brake_right(&mut self, simconnect_value: u32) { - self.brake_right_sim_input = sim_connect_32k_pos_to_f64(simconnect_value); - } - - fn brake_left(&mut self) -> f64 { - self.brake_left_sim_input - .max(self.brake_left_sim_input_keyboard) - } - - fn brake_right(&mut self) -> f64 { - self.brake_right_sim_input - .max(self.brake_right_sim_input_keyboard) - } - - fn set_brake_right_output(&mut self, brake_force_factor: f64) { - self.brake_right_output_to_sim = brake_force_factor; - } - - fn set_brake_left_output(&mut self, brake_force_factor: f64) { - self.brake_left_output_to_sim = brake_force_factor; - } - - fn receive_a_park_brake_event(&mut self) { - self.parking_brake_lever_is_set = !self.parking_brake_lever_is_set; - } - - fn receive_a_park_brake_set_event(&mut self, data: u32) { - self.parking_brake_lever_is_set = data == 1; - } - - fn get_brake_right_output_converted_in_simconnect_format(&mut self) -> u32 { - f64_to_sim_connect_32k_pos(self.brake_right_output_to_sim) - } - - fn get_brake_left_output_converted_in_simconnect_format(&mut self) -> u32 { - f64_to_sim_connect_32k_pos(self.brake_left_output_to_sim) - } - - fn is_park_brake_set(&self) -> f64 { - if self.parking_brake_lever_is_set { - 1. - } else { - 0. - } - } -} -impl SimulatorAspect for Brakes { - fn read(&mut self, identifier: &VariableIdentifier) -> Option { - if identifier == &self.park_brak_lever_pos_id { - Some(self.is_park_brake_set()) - } else if identifier == &self.left_brake_pedal_input_id { - Some(self.brake_left()) - } else if identifier == &self.right_brake_pedal_input_id { - Some(self.brake_right()) - } else { - None - } - } - - fn write(&mut self, identifier: &VariableIdentifier, value: f64) -> bool { - if identifier == &self.brake_left_force_factor_id { - self.set_brake_left_output(value); - true - } else if identifier == &self.brake_right_force_factor_id { - self.set_brake_right_output(value); - true - } else { - false - } - } - - fn handle_message(&mut self, message: &SimConnectRecv) -> bool { - match message { - SimConnectRecv::Event(e) => { - if e.id() == self.id_brake_left { - self.set_brake_left(e.data()); - true - } else if e.id() == self.id_brake_right { - self.set_brake_right(e.data()); - true - } else if e.id() == self.id_parking_brake { - self.receive_a_park_brake_event(); - true - } else if e.id() == self.id_parking_brake_set { - self.receive_a_park_brake_set_event(e.data()); - true - } else if e.id() == self.id_brake_keyboard { - self.set_brake_left_key_pressed(); - self.set_brake_right_key_pressed(); - true - } else if e.id() == self.id_brake_left_keyboard { - self.set_brake_left_key_pressed(); - true - } else if e.id() == self.id_brake_right_keyboard { - self.set_brake_right_key_pressed(); - true - } else { - false - } - } - _ => false, - } - } - - fn pre_tick(&mut self, delta: Duration) { - self.synchronise_with_sim(); - self.update_keyboard_inputs(delta); - } - - fn post_tick(&mut self, sim_connect: &mut SimConnect) -> Result<(), Box> { - self.reset_keyboard_events(); - self.transmit_client_events(sim_connect)?; - self.transmit_masked_inputs(); - - Ok(()) - } -} - -struct NoseWheelSteering { - realistic_tiller_axis_var: NamedVariable, - is_realistic_tiller_mode: bool, - - tiller_handle_position_id: VariableIdentifier, - tiller_handle_position_var: NamedVariable, - - rudder_pedal_position_id: VariableIdentifier, - - rudder_position_var: AircraftVariable, - rudder_position: f64, - - nose_wheel_position_id: VariableIdentifier, - nose_wheel_position_var: NamedVariable, - nose_wheel_position: f64, - - rudder_pedal_position_var: NamedVariable, - rudder_pedal_position: f64, - - tiller_handle_position_event: sys::DWORD, - tiller_handle_position: f64, - - nose_wheel_angle_event: sys::DWORD, - nose_wheel_angle_inc_event: sys::DWORD, - nose_wheel_angle_dec_event: sys::DWORD, - - pedal_disconnect_event: sys::DWORD, - pedal_disconnect_id: VariableIdentifier, - pedal_disconnect: bool, -} - -impl MsfsAspectCtor for NoseWheelSteering { - fn new( - registry: &mut MsfsVariableRegistry, - sim_connect: &mut SimConnect, - ) -> Result> { - Ok(Self { - realistic_tiller_axis_var: NamedVariable::from("A32NX_REALISTIC_TILLER_ENABLED"), - is_realistic_tiller_mode: false, - - tiller_handle_position_id: registry.get("TILLER_HANDLE_POSITION".to_owned()), - tiller_handle_position_var: NamedVariable::from("A32NX_TILLER_HANDLE_POSITION"), - - rudder_pedal_position_id: registry.get("RUDDER_PEDAL_POSITION".to_owned()), - - rudder_position_var: AircraftVariable::from("RUDDER POSITION", "Position", 0)?, - rudder_position: 0.5, - - nose_wheel_position_id: registry.get("NOSE_WHEEL_POSITION".to_owned()), - nose_wheel_position_var: NamedVariable::from("A32NX_NOSE_WHEEL_POSITION"), - nose_wheel_position: 0., - - rudder_pedal_position_var: NamedVariable::from("A32NX_RUDDER_PEDAL_POSITION"), - rudder_pedal_position: 0.5, - - tiller_handle_position_event: sim_connect - .map_client_event_to_sim_event("AXIS_MIXTURE4_SET", true)?, - tiller_handle_position: 0.5, - - nose_wheel_angle_event: sim_connect - .map_client_event_to_sim_event("STEERING_SET", true)?, - nose_wheel_angle_inc_event: sim_connect - .map_client_event_to_sim_event("STEERING_INC", true)?, - nose_wheel_angle_dec_event: sim_connect - .map_client_event_to_sim_event("STEERING_DEC", true)?, - - pedal_disconnect_event: sim_connect - .map_client_event_to_sim_event("TOGGLE_WATER_RUDDER", true)?, - pedal_disconnect_id: registry.get("TILLER_PEDAL_DISCONNECT".to_owned()), - pedal_disconnect: false, - }) - } -} -impl NoseWheelSteering { - const MAX_CONTROLLABLE_STEERING_ANGLE_DEGREES: f64 = 75.; - const MAX_MSFS_STEERING_ANGLE_DEGREES: f64 = 90.; - const STEERING_ANIMATION_TOTAL_RANGE_DEGREES: f64 = 360.; - - const TILLER_KEYBOARD_INCREMENTS: f64 = 0.05; - - fn set_tiller_handle(&mut self, simconnect_value: u32) { - self.tiller_handle_position = sim_connect_32k_pos_to_f64(simconnect_value); - } - - fn decrement_tiller(&mut self) { - self.tiller_handle_position -= Self::TILLER_KEYBOARD_INCREMENTS; - self.tiller_handle_position = self.tiller_handle_position.min(1.).max(0.); - - self.tiller_key_event_centering(); - } - - fn increment_tiller(&mut self) { - self.tiller_handle_position += Self::TILLER_KEYBOARD_INCREMENTS; - self.tiller_handle_position = self.tiller_handle_position.min(1.).max(0.); - - self.tiller_key_event_centering(); - } - - fn tiller_key_event_centering(&mut self) { - if self.tiller_handle_position < 0.5 + Self::TILLER_KEYBOARD_INCREMENTS - && self.tiller_handle_position > 0.5 - Self::TILLER_KEYBOARD_INCREMENTS - { - self.tiller_handle_position = 0.5; - } - } - - fn set_pedal_disconnect(&mut self, is_disconnected: bool) { - self.pedal_disconnect = is_disconnected; - } - - /// Steering position is [-1;1] -1 is left, 0 is straight - fn set_steering_position(&mut self, steering_position: f64) { - self.nose_wheel_position = steering_position; - } - - /// Tiller position in [-1;1] range, -1 is left - fn tiller_handle_position(&self) -> f64 { - self.tiller_handle_position * 2. - 1. - } - - /// Rudder pedal position in [-1;1] range, -1 is left - fn rudder_pedal_position(&self) -> f64 { - self.rudder_pedal_position * 2. - 1. - } - - fn set_realistic_tiller_mode(&mut self, is_active: bool) { - self.is_realistic_tiller_mode = is_active; - } - - fn synchronise_with_sim(&mut self) { - let rudder_percent: f64 = self.rudder_pedal_position_var.get_value(); - self.rudder_pedal_position = (rudder_percent + 100.) / 200.; - - let rudder_position: f64 = self.rudder_position_var.get(); - self.rudder_position = (rudder_position + 1.) / 2.; - - let realistic_mode: f64 = self.realistic_tiller_axis_var.get_value(); - self.set_realistic_tiller_mode(realistic_mode > 0.); - } - - fn final_tiller_position_sent_to_systems(&self) -> f64 { - if self.is_realistic_tiller_mode { - self.tiller_handle_position() - } else { - if !self.pedal_disconnect { - self.rudder_pedal_position() - } else { - 0. - } - } - } - - fn final_rudder_pedal_position_sent_to_systems(&self) -> f64 { - if self.is_realistic_tiller_mode { - self.rudder_pedal_position() - } else { - 0. - } - } - - fn steering_demand_to_msfs_from_steering_angle(&self) -> f64 { - // Steering in msfs is the max we want rescaled to the max in msfs - let steering_ratio_converted = self.nose_wheel_position - * Self::MAX_CONTROLLABLE_STEERING_ANGLE_DEGREES - / Self::MAX_MSFS_STEERING_ANGLE_DEGREES - / 2. - + 0.5; - - // Steering demand is reverted in msfs so we do 1 - angle. - // Then we hack msfs by adding the rudder value that it will always substract internally - // This way we end up with actual angle we required - (1. - steering_ratio_converted) + (self.rudder_position - 0.5) - } - - fn steering_animation_to_msfs_from_steering_angle(&self) -> f64 { - ((self.nose_wheel_position * Self::MAX_CONTROLLABLE_STEERING_ANGLE_DEGREES - / (Self::STEERING_ANIMATION_TOTAL_RANGE_DEGREES / 2.)) - / 2.) - + 0.5 - } - - fn write_animation_position_to_sim(&self) { - self.tiller_handle_position_var - .set_value((self.final_tiller_position_sent_to_systems() + 1.) / 2.); - - self.nose_wheel_position_var - .set_value(self.steering_animation_to_msfs_from_steering_angle()); - } - - fn transmit_client_events( - &mut self, - sim_connect: &mut SimConnect, - ) -> Result<(), Box> { - sim_connect.transmit_client_event( - SIMCONNECT_OBJECT_ID_USER, - self.nose_wheel_angle_event, - f64_to_sim_connect_32k_pos(self.steering_demand_to_msfs_from_steering_angle()), - )?; - - Ok(()) - } -} -impl SimulatorAspect for NoseWheelSteering { - fn read(&mut self, identifier: &VariableIdentifier) -> Option { - if identifier == &self.tiller_handle_position_id { - Some(self.final_tiller_position_sent_to_systems()) - } else if identifier == &self.rudder_pedal_position_id { - Some(self.final_rudder_pedal_position_sent_to_systems()) - } else if identifier == &self.pedal_disconnect_id { - Some(self.pedal_disconnect as u8 as f64) - } else { - None - } - } - - fn write(&mut self, identifier: &VariableIdentifier, value: f64) -> bool { - if identifier == &self.nose_wheel_position_id { - self.set_steering_position(value); - true - } else { - false - } - } - - fn handle_message(&mut self, message: &SimConnectRecv) -> bool { - match message { - SimConnectRecv::Event(e) => { - if e.id() == self.tiller_handle_position_event { - self.set_tiller_handle(e.data()); - true - } else if e.id() == self.pedal_disconnect_event { - self.set_pedal_disconnect(true); - true - } else if e.id() == self.nose_wheel_angle_dec_event { - self.decrement_tiller(); - true - } else if e.id() == self.nose_wheel_angle_inc_event { - self.increment_tiller(); - true - } else { - false - } - } - _ => false, - } - } - - fn pre_tick(&mut self, _: Duration) { - self.synchronise_with_sim(); - } - - fn post_tick(&mut self, sim_connect: &mut SimConnect) -> Result<(), Box> { - self.transmit_client_events(sim_connect)?; - self.write_animation_position_to_sim(); - self.set_pedal_disconnect(false); - - Ok(()) - } -} - -struct CargoDoors { - fwd_door_cargo_position_id: VariableIdentifier, - fwd_door_cargo_open_req_id: VariableIdentifier, - - forward_cargo_door_position: NamedVariable, - forward_cargo_door_sim_position_request: AircraftVariable, - fwd_position: f64, - forward_cargo_door_open_req: f64, -} -impl MsfsAspectCtor for CargoDoors { - fn new( - registry: &mut MsfsVariableRegistry, - _: &mut SimConnect, - ) -> Result> { - Ok(Self { - fwd_door_cargo_position_id: registry.get("FWD_DOOR_CARGO_POSITION".to_owned()), - fwd_door_cargo_open_req_id: registry.get("FWD_DOOR_CARGO_OPEN_REQ".to_owned()), - - forward_cargo_door_position: NamedVariable::from("A32NX_FWD_DOOR_CARGO_POSITION"), - forward_cargo_door_sim_position_request: AircraftVariable::from( - "INTERACTIVE POINT OPEN", - "Position", - 5, - )?, - fwd_position: 0., - forward_cargo_door_open_req: 0., - }) - } -} - -impl CargoDoors { - fn set_forward_door_postition(&mut self, value: f64) { - self.fwd_position = value; - } - - fn set_in_sim_position_request(&mut self, position_requested: f64) { - if position_requested > 0. { - self.forward_cargo_door_open_req = 1.; - } else { - self.forward_cargo_door_open_req = 0.; - } - } -} -impl SimulatorAspect for CargoDoors { - fn write(&mut self, identifier: &VariableIdentifier, value: f64) -> bool { - if identifier == &self.fwd_door_cargo_position_id { - self.set_forward_door_postition(value); - true - } else { - false - } - } - - fn read(&mut self, identifier: &VariableIdentifier) -> Option { - if identifier == &self.fwd_door_cargo_open_req_id { - Some(self.forward_cargo_door_open_req) - } else if identifier == &self.fwd_door_cargo_position_id { - Some(self.fwd_position) - } else { - None - } - } - - fn pre_tick(&mut self, _: Duration) { - let read_val = self.forward_cargo_door_sim_position_request.get(); - self.set_in_sim_position_request(read_val); - } - - fn post_tick(&mut self, _: &mut SimConnect) -> Result<(), Box> { - self.forward_cargo_door_position - .set_value(self.fwd_position); - - Ok(()) - } -} diff --git a/src/systems/a320_systems_wasm/src/nose_wheel_steering.rs b/src/systems/a320_systems_wasm/src/nose_wheel_steering.rs new file mode 100644 index 00000000000..79c950c7d2f --- /dev/null +++ b/src/systems/a320_systems_wasm/src/nose_wheel_steering.rs @@ -0,0 +1,179 @@ +use std::error::Error; +use systems::shared::to_bool; +use systems_wasm::aspects::{ + EventToVariableMapping, ExecuteOn, MsfsAspectBuilder, VariableToEventMapping, + VariableToEventWriteOn, +}; +use systems_wasm::Variable; + +pub(super) fn nose_wheel_steering(builder: &mut MsfsAspectBuilder) -> Result<(), Box> { + // The rudder pedals should start in a centered position. + builder.init_variable(Variable::aspect("RAW_RUDDER_PEDAL_POSITION"), 0.5); + + builder.map( + ExecuteOn::PreTick, + Variable::named("RUDDER_PEDAL_POSITION"), + // Convert rudder pedal position to [-1;1], -1 is left + |value| ((value + 100.) / 200.) * 2. - 1., + Variable::aspect("RAW_RUDDER_PEDAL_POSITION"), + ); + + builder.map_many( + ExecuteOn::PostTick, + vec![ + Variable::named("REALISTIC_TILLER_ENABLED"), + Variable::aspect("RAW_RUDDER_PEDAL_POSITION"), + ], + |values| { + let realistic_tiller_enabled = to_bool(values[0]); + let rudder_pedal_position = values[1]; + if realistic_tiller_enabled { + rudder_pedal_position + } else { + 0. + } + }, + Variable::aspect("RUDDER_PEDAL_POSITION_RATIO"), + ); + + // The tiller handle should start in a centered position. + builder.init_variable(Variable::aspect("RAW_TILLER_HANDLE_POSITION"), 0.5); + + // Lacking a better event to bind to, we've picked a mixture axis for setting the + // tiller handle position. + builder.event_to_variable( + "AXIS_MIXTURE4_SET", + EventToVariableMapping::EventData32kPosition, + Variable::aspect("RAW_TILLER_HANDLE_POSITION"), + |options| options.mask(), + )?; + + const TILLER_KEYBOARD_INCREMENTS: f64 = 0.05; + builder.event_to_variable( + "STEERING_INC", + EventToVariableMapping::CurrentValueToValue(|current_value| { + recenter_when_close_to_center( + (current_value + TILLER_KEYBOARD_INCREMENTS).min(1.), + TILLER_KEYBOARD_INCREMENTS, + ) + }), + Variable::aspect("RAW_TILLER_HANDLE_POSITION"), + |options| options.mask(), + )?; + builder.event_to_variable( + "STEERING_DEC", + EventToVariableMapping::CurrentValueToValue(|current_value| { + recenter_when_close_to_center( + (current_value - TILLER_KEYBOARD_INCREMENTS).max(0.), + TILLER_KEYBOARD_INCREMENTS, + ) + }), + Variable::aspect("RAW_TILLER_HANDLE_POSITION"), + |options| options.mask(), + )?; + + // Lacking a better event to bind to, we've picked the toggle water rudder event for + // disconnecting the rudder pedals via the PEDALS DISC button on the tiller. + builder.event_to_variable( + "TOGGLE_WATER_RUDDER", + EventToVariableMapping::Value(1.), + Variable::aspect("TILLER_PEDAL_DISCONNECT"), + |options| options.mask().afterwards_reset_to(0.), + )?; + + builder.map_many( + ExecuteOn::PostTick, + vec![ + Variable::named("REALISTIC_TILLER_ENABLED"), + Variable::aspect("RAW_RUDDER_PEDAL_POSITION"), + Variable::aspect("RAW_TILLER_HANDLE_POSITION"), + Variable::aspect("TILLER_PEDAL_DISCONNECT"), + ], + |values| { + let realistic_tiller_enabled = to_bool(values[0]); + let rudder_pedal_position = values[1]; + let tiller_handle_position = values[2]; + let tiller_pedal_disconnect = to_bool(values[3]); + + if realistic_tiller_enabled { + // Convert tiller handle position to [-1;1], -1 is left + tiller_handle_position * 2. - 1. + } else { + if !tiller_pedal_disconnect { + rudder_pedal_position + } else { + 0. + } + } + }, + Variable::named("TILLER_HANDLE_POSITION"), + ); + + builder.map( + ExecuteOn::PostTick, + Variable::aspect("NOSE_WHEEL_POSITION_RATIO"), + steering_animation_to_msfs_from_steering_angle, + Variable::named("NOSE_WHEEL_POSITION"), + ); + + builder.map_many( + ExecuteOn::PostTick, + vec![ + Variable::aspect("NOSE_WHEEL_POSITION_RATIO"), + Variable::aircraft("RUDDER POSITION", "Position", 0), + ], + |values| { + let nose_wheel_position = values[0]; + let rudder_position = (values[1] + 1.) / 2.; + + steering_demand_to_msfs_from_steering_angle(nose_wheel_position, rudder_position) + }, + Variable::aspect("STEERING_ANGLE"), + ); + + builder.variable_to_event( + Variable::aspect("STEERING_ANGLE"), + VariableToEventMapping::EventData32kPosition, + VariableToEventWriteOn::EveryTick, + "STEERING_SET", + )?; + + Ok(()) +} + +fn recenter_when_close_to_center(value: f64, increment: f64) -> f64 { + if value < 0.5 + increment && value > 0.5 - increment { + 0.5 + } else { + value + } +} + +const MAX_CONTROLLABLE_STEERING_ANGLE_DEGREES: f64 = 75.; + +fn steering_animation_to_msfs_from_steering_angle(nose_wheel_position: f64) -> f64 { + const STEERING_ANIMATION_TOTAL_RANGE_DEGREES: f64 = 360.; + + ((nose_wheel_position * MAX_CONTROLLABLE_STEERING_ANGLE_DEGREES + / (STEERING_ANIMATION_TOTAL_RANGE_DEGREES / 2.)) + / 2.) + + 0.5 +} + +fn steering_demand_to_msfs_from_steering_angle( + nose_wheel_position: f64, + rudder_position: f64, +) -> f64 { + const MAX_MSFS_STEERING_ANGLE_DEGREES: f64 = 90.; + + // Steering in msfs is the max we want rescaled to the max in msfs + let steering_ratio_converted = nose_wheel_position * MAX_CONTROLLABLE_STEERING_ANGLE_DEGREES + / MAX_MSFS_STEERING_ANGLE_DEGREES + / 2. + + 0.5; + + // Steering demand is reverted in msfs so we do 1 - angle. + // Then we hack msfs by adding the rudder value that it will always substract internally + // This way we end up with actual angle we required + (1. - steering_ratio_converted) + (rudder_position - 0.5) +} diff --git a/src/systems/systems/src/hydraulic/nose_steering.rs b/src/systems/systems/src/hydraulic/nose_steering.rs index 17ff7aaf8ec..5bb3c8e7075 100644 --- a/src/systems/systems/src/hydraulic/nose_steering.rs +++ b/src/systems/systems/src/hydraulic/nose_steering.rs @@ -125,7 +125,7 @@ impl SteeringActuator { angular_to_linear_ratio: Ratio, ) -> Self { Self { - position_id: context.get_identifier("NOSE_WHEEL_POSITION".to_owned()), + position_id: context.get_identifier("NOSE_WHEEL_POSITION_RATIO".to_owned()), current_speed: LowPassFilter::::new( Self::CURRENT_SPEED_FILTER_TIMECONST, @@ -402,7 +402,7 @@ mod tests { test_bed.run(); - assert!(test_bed.contains_variable_with_name("NOSE_WHEEL_POSITION")); + assert!(test_bed.contains_variable_with_name("NOSE_WHEEL_POSITION_RATIO")); } #[test] @@ -419,7 +419,7 @@ mod tests { actuator_position_init )); - let normalized_position: f64 = test_bed.read_by_name("NOSE_WHEEL_POSITION"); + let normalized_position: f64 = test_bed.read_by_name("NOSE_WHEEL_POSITION_RATIO"); assert!(normalized_position == 0.); } diff --git a/src/systems/systems/src/shared/mod.rs b/src/systems/systems/src/shared/mod.rs index 54f156c03f7..8d0b2296db9 100644 --- a/src/systems/systems/src/shared/mod.rs +++ b/src/systems/systems/src/shared/mod.rs @@ -408,6 +408,15 @@ pub fn interpolation(xs: &[f64], ys: &[f64], intermediate_x: f64) -> f64 { } } +/// Converts a given `bool` value into an `f64` representing that boolean value in the simulator. +pub fn from_bool(value: bool) -> f64 { + if value { + 1.0 + } else { + 0.0 + } +} + pub fn to_bool(value: f64) -> bool { (value - 1.).abs() < f64::EPSILON } diff --git a/src/systems/systems/src/simulation/mod.rs b/src/systems/systems/src/simulation/mod.rs index 302469882af..124c23d0f8b 100644 --- a/src/systems/systems/src/simulation/mod.rs +++ b/src/systems/systems/src/simulation/mod.rs @@ -2,7 +2,7 @@ use std::time::Duration; mod update_context; use crate::electrical::{ElectricalElementIdentifier, ElectricalElementIdentifierProvider}; -use crate::shared::ElectricalBusType; +use crate::shared::{from_bool, ElectricalBusType}; use crate::{ electrical::Electricity, failures::FailureType, @@ -39,9 +39,9 @@ pub trait VariableRegistry { pub struct VariableIdentifier(u8, usize); impl VariableIdentifier { - pub fn new(identifier_type: u8) -> Self { + pub fn new>(variable_type: T) -> Self { Self { - 0: identifier_type, + 0: variable_type.into(), 1: 0, } } @@ -501,15 +501,6 @@ impl<'a> Writer for SimulatorWriter<'a> { } } -/// Converts a given `bool` value into an `f64` representing that boolean value in the simulator. -fn from_bool(value: bool) -> f64 { - if value { - 1.0 - } else { - 0.0 - } -} - pub trait Read { /// Reads a value from the simulator. /// # Examples diff --git a/src/systems/systems_wasm/Cargo.toml b/src/systems/systems_wasm/Cargo.toml index 1530c5ca714..f07069fa474 100644 --- a/src/systems/systems_wasm/Cargo.toml +++ b/src/systems/systems_wasm/Cargo.toml @@ -14,3 +14,4 @@ uom = "0.30.0" systems = { path = "../systems" } msfs = { git = "https://github.com/flybywiresim/msfs-rs", branch = "main" } fxhash = "0.2.1" +enum_dispatch = "0.3.7" \ No newline at end of file diff --git a/src/systems/systems_wasm/src/aspects.rs b/src/systems/systems_wasm/src/aspects.rs new file mode 100644 index 00000000000..d7a8e2aa087 --- /dev/null +++ b/src/systems/systems_wasm/src/aspects.rs @@ -0,0 +1,852 @@ +use crate::{ + f64_to_sim_connect_32k_pos, sim_connect_32k_pos_to_f64, Aspect, MsfsVariableRegistry, Variable, +}; +use enum_dispatch::enum_dispatch; +use msfs::sim_connect::{SimConnect, SimConnectRecv, SIMCONNECT_OBJECT_ID_USER}; +use std::error::Error; +use std::time::{Duration, Instant}; +use systems::simulation::VariableIdentifier; + +/// Type used to configure and build an [Aspect]. +/// +/// It should be noted that the resulting [Aspect] executes its tasks in the order in which they +/// were declared. Declaration order is important when one action depends on another action. +/// When e.g. a variable that is mapped should then be written to an event, be sure to +/// declare the mapping before the event writing. +pub struct MsfsAspectBuilder<'a, 'b> { + sim_connect: &'a mut SimConnect<'b>, + variables: &'a mut MsfsVariableRegistry, + message_handlers: Vec, + actions: Vec<(VariableAction, ExecuteOn)>, +} + +impl<'a, 'b> MsfsAspectBuilder<'a, 'b> { + pub fn new( + sim_connect: &'a mut SimConnect<'b>, + variables: &'a mut MsfsVariableRegistry, + ) -> Self { + Self { + sim_connect, + variables, + message_handlers: Default::default(), + actions: Default::default(), + } + } + + pub fn build(self) -> MsfsAspect { + let aspect = MsfsAspect::new(self.message_handlers, self.actions); + + aspect + } + + /// Initialise the variable with the given value. + pub fn init_variable(&mut self, variable: Variable, value: f64) { + Self::precondition_not_aircraft_variable(&variable); + + let identifier = self.variables.register(&variable); + self.variables.write(&identifier, value); + } + + /// Copy a variable's value to another variable. + pub fn copy(&mut self, input: Variable, output: Variable) { + Self::precondition_not_aircraft_variable(&output); + + let input = self.variables.register(&input); + let output = self.variables.register(&output); + + self.actions.push(( + Map::new(input, |value| value, output).into(), + ExecuteOn::PreTick, + )); + } + + /// Map a variable's value to another variable, applying the given function in the process. + pub fn map( + &mut self, + execute_on: ExecuteOn, + input: Variable, + func: fn(f64) -> f64, + output: Variable, + ) { + Self::precondition_not_aircraft_variable(&output); + + let inputs = self.variables.register(&input); + let output = self.variables.register(&output); + + self.actions + .push((Map::new(inputs, func, output).into(), execute_on)); + } + + /// Map a set of variable values to another variable. + pub fn map_many( + &mut self, + execute_on: ExecuteOn, + inputs: Vec, + func: fn(&[f64]) -> f64, + output: Variable, + ) { + Self::precondition_not_aircraft_variable(&output); + + let inputs = self.variables.register_many(&inputs); + let output = self.variables.register(&output); + + self.actions + .push((MapMany::new(inputs, func, output).into(), execute_on)); + } + + /// Reduce a set of variable values into one output value and write it to a variable. + pub fn reduce( + &mut self, + execute_on: ExecuteOn, + inputs: Vec, + init: f64, + func: fn(f64, f64) -> f64, + output: Variable, + ) { + Self::precondition_not_aircraft_variable(&output); + + let inputs = self.variables.register_many(&inputs); + let output = self.variables.register(&output); + + self.actions + .push((Reduce::new(inputs, init, func, output).into(), execute_on)); + } + + /// Write a set of variables to an object. + pub fn variables_to_object(&mut self, instance: Box) { + let variables = self.variables.register_many(&instance.variables()); + + self.actions.push(( + ToObject::new(instance, variables).into(), + ExecuteOn::PostTick, + )); + } + + /// Convert event occurrences to a variable. + /// + /// If you want to write a variable back to the same event, then use the + /// event id returned by this method and pass it to [Self::variable_to_event_id]. + pub fn event_to_variable( + &mut self, + event_name: &str, + mapping: EventToVariableMapping, + target: Variable, + configure_options: fn(EventToVariableOptions) -> EventToVariableOptions, + ) -> Result> { + Self::precondition_not_aircraft_variable(&target); + + let target = self.variables.register(&target); + + let event_to_variable = EventToVariable::new( + &mut self.sim_connect, + event_name, + mapping, + target, + configure_options(EventToVariableOptions::default()), + )?; + + let event_id = event_to_variable.event_id; + + self.message_handlers.push(event_to_variable.into()); + + Ok(event_id) + } + + /// Write the variable's value to an event. If you use [Self::event_to_variable] for the same + /// event, then you should use [Self::variable_to_event_id] instead. + pub fn variable_to_event( + &mut self, + input: Variable, + mapping: VariableToEventMapping, + write_on: VariableToEventWriteOn, + event_name: &str, + ) -> Result<(), Box> { + let input = self.variables.register(&input); + + self.actions.push(( + ToEvent::new(&mut self.sim_connect, input, mapping, write_on, event_name)?.into(), + ExecuteOn::PostTick, + )); + + Ok(()) + } + + /// Write the variable's value to an event with the given event id. This function should be used + /// when you previously acquired an event id using [Self::event_to_variable]. + pub fn variable_to_event_id( + &mut self, + input: Variable, + mapping: VariableToEventMapping, + write_on: VariableToEventWriteOn, + event_id: u32, + ) { + let input = self.variables.register(&input); + + self.actions.push(( + ToEvent::new_with_event_id(input, mapping, write_on, event_id).into(), + ExecuteOn::PostTick, + )); + } + + fn precondition_not_aircraft_variable(variable: &Variable) { + if matches!(variable, Variable::Aircraft(..)) { + eprintln!("Writing to variable '{}' is unsupported.", variable); + } + } +} + +pub struct MsfsAspect { + message_handlers: Vec, + actions: Vec<(VariableAction, ExecuteOn)>, +} + +impl MsfsAspect { + fn new( + message_handlers: Vec, + actions: Vec<(VariableAction, ExecuteOn)>, + ) -> Self { + Self { + message_handlers, + actions, + } + } + + fn execute_actions( + &mut self, + sim_connect: &mut SimConnect, + execute_moment: ExecuteOn, + variables: &mut MsfsVariableRegistry, + ) -> Result<(), Box> { + self.actions + .iter_mut() + .try_for_each(|(action, execute_on)| { + if *execute_on == execute_moment { + action.execute(sim_connect, variables)?; + } + + Ok(()) + }) + } +} + +impl Aspect for MsfsAspect { + fn handle_message( + &mut self, + message: &SimConnectRecv, + variables: &mut MsfsVariableRegistry, + ) -> bool { + self.message_handlers + .iter_mut() + .any(|handler| handler.handle(message, variables)) + } + + fn pre_tick( + &mut self, + variables: &mut MsfsVariableRegistry, + sim_connect: &mut SimConnect, + delta: Duration, + ) -> Result<(), Box> { + self.message_handlers + .iter_mut() + .for_each(|ev| ev.pre_tick(variables, delta)); + + self.execute_actions(sim_connect, ExecuteOn::PreTick, variables)?; + + Ok(()) + } + + fn post_tick( + &mut self, + variables: &mut MsfsVariableRegistry, + sim_connect: &mut SimConnect, + ) -> Result<(), Box> { + self.execute_actions(sim_connect, ExecuteOn::PostTick, variables)?; + + self.message_handlers + .iter_mut() + .for_each(|ev| ev.post_tick(variables)); + + Ok(()) + } +} + +#[derive(Clone, Copy, PartialEq)] +/// Declares when to execute the action. +pub enum ExecuteOn { + PreTick, + PostTick, +} + +#[enum_dispatch] +enum VariableAction { + Map, + MapMany, + Reduce, + ToObject, + ToEvent, +} + +#[enum_dispatch(VariableAction)] +trait ExecutableVariableAction { + fn execute( + &mut self, + sim_connect: &mut SimConnect, + variables: &mut MsfsVariableRegistry, + ) -> Result<(), Box>; +} + +struct Map { + input_variable_identifier: VariableIdentifier, + func: fn(f64) -> f64, + output_variable_identifier: VariableIdentifier, +} + +impl Map { + fn new( + input_variable_identifier: VariableIdentifier, + func: fn(f64) -> f64, + output_variable_identifier: VariableIdentifier, + ) -> Self { + Self { + input_variable_identifier, + func, + output_variable_identifier, + } + } +} + +impl ExecutableVariableAction for Map { + fn execute( + &mut self, + _: &mut SimConnect, + variables: &mut MsfsVariableRegistry, + ) -> Result<(), Box> { + let value = match variables.read(&self.input_variable_identifier) { + Some(value) => value, + None => panic!("Attempted to map a variable which is unavailable."), + }; + + variables.write(&self.output_variable_identifier, (self.func)(value)); + + Ok(()) + } +} + +fn precondition_multiple_identifiers(action_name: &str, identifiers: &[VariableIdentifier]) { + if identifiers.len() < 2 { + eprintln!( + "{} requires at least 2 input variables. {} {} provided.", + action_name, + identifiers.len(), + if identifiers.len() == 1 { + "was" + } else { + "were" + } + ); + } +} + +struct MapMany { + input_variable_identifiers: Vec, + func: fn(&[f64]) -> f64, + output_variable_identifier: VariableIdentifier, +} + +impl MapMany { + fn new( + input_variable_identifiers: Vec, + func: fn(&[f64]) -> f64, + output_variable_identifier: VariableIdentifier, + ) -> Self { + precondition_multiple_identifiers("MapMany", &input_variable_identifiers); + + Self { + input_variable_identifiers, + func, + output_variable_identifier, + } + } +} + +impl ExecutableVariableAction for MapMany { + fn execute( + &mut self, + _: &mut SimConnect, + variables: &mut MsfsVariableRegistry, + ) -> Result<(), Box> { + let values: Vec = variables + .read_many(&self.input_variable_identifiers) + .iter() + .map(|&x| x.unwrap()) + .collect(); + let result = (self.func)(&values); + variables.write(&self.output_variable_identifier, result); + + Ok(()) + } +} + +struct Reduce { + input_variable_identifiers: Vec, + init: f64, + func: fn(f64, f64) -> f64, + output_variable_identifier: VariableIdentifier, +} + +impl Reduce { + fn new( + input_variable_identifiers: Vec, + init: f64, + func: fn(f64, f64) -> f64, + output_variable_identifier: VariableIdentifier, + ) -> Self { + precondition_multiple_identifiers("Reduce", &input_variable_identifiers); + + Self { + input_variable_identifiers, + init, + func, + output_variable_identifier, + } + } +} + +impl ExecutableVariableAction for Reduce { + fn execute( + &mut self, + _: &mut SimConnect, + variables: &mut MsfsVariableRegistry, + ) -> Result<(), Box> { + let values: Vec = variables + .read_many(&self.input_variable_identifiers) + .iter() + .map(|&x| x.unwrap()) + .collect(); + let result = values.into_iter().fold(self.init, self.func); + variables.write(&self.output_variable_identifier, result); + + Ok(()) + } +} + +pub fn max(accumulator: f64, item: f64) -> f64 { + accumulator.max(item) +} + +pub fn min(accumulator: f64, item: f64) -> f64 { + accumulator.min(item) +} + +pub trait VariablesToObject { + fn variables(&self) -> Vec; + fn write(&mut self, values: Vec); + fn set_data_on_sim_object(&self, sim_connect: &mut SimConnect) -> Result<(), Box>; +} + +struct ToObject { + target_object: Box, + variables: Vec, +} + +impl ToObject { + fn new(target_object: Box, variables: Vec) -> Self { + Self { + target_object, + variables, + } + } +} + +impl ExecutableVariableAction for ToObject { + fn execute( + &mut self, + sim_connect: &mut SimConnect, + variables: &mut MsfsVariableRegistry, + ) -> Result<(), Box> { + let values: Vec = self + .variables + .iter() + .map( + |variable_identifier| match variables.read(variable_identifier) { + Some(value) => value, + None => { + panic!("Attempted to access variables which are unavailable.") + } + }, + ) + .collect(); + + self.target_object.write(values); + self.target_object.set_data_on_sim_object(sim_connect)?; + + Ok(()) + } +} + +#[macro_export] +macro_rules! set_data_on_sim_object { + () => { + fn set_data_on_sim_object( + &self, + sim_connect: &mut SimConnect, + ) -> Result<(), Box> { + sim_connect.set_data_on_sim_object(SIMCONNECT_OBJECT_ID_USER, self)?; + Ok(()) + } + }; +} + +#[enum_dispatch] +enum Debounce { + None(NoDebounce), + Leading(LeadingDebounce), +} + +#[enum_dispatch(Debounce)] +trait Debouncer { + fn should_handle(&self) -> bool; + fn notify_handled(&mut self); + fn post_tick(&mut self, variables: &mut MsfsVariableRegistry); +} + +#[derive(Default)] +struct NoDebounce { + event_handled_before_tick: bool, + reset_to: Option<(VariableIdentifier, f64)>, +} + +impl NoDebounce { + fn new(reset_to: Option<(VariableIdentifier, f64)>) -> Self { + Self { + event_handled_before_tick: false, + reset_to, + } + } +} + +impl Debouncer for NoDebounce { + fn should_handle(&self) -> bool { + true + } + + fn notify_handled(&mut self) { + self.event_handled_before_tick = true; + } + + fn post_tick(&mut self, variables: &mut MsfsVariableRegistry) { + if let Some((variable, value)) = &self.reset_to { + variables.write(variable, *value); + } + + self.event_handled_before_tick = false; + } +} + +struct LeadingDebounce { + duration: Duration, + handled_at: Option, + reset_to: Option<(VariableIdentifier, f64)>, +} + +impl LeadingDebounce { + fn new(duration: Duration, reset_to: Option<(VariableIdentifier, f64)>) -> Self { + Self { + duration, + handled_at: None, + reset_to, + } + } + + fn exceeded_debounce_duration(&self) -> bool { + self.handled_at + .map(|instant| instant.elapsed() > self.duration) + .unwrap_or(true) + } +} + +impl Debouncer for LeadingDebounce { + fn should_handle(&self) -> bool { + self.exceeded_debounce_duration() + } + + fn notify_handled(&mut self) { + self.handled_at = Some(Instant::now()); + } + + fn post_tick(&mut self, variables: &mut MsfsVariableRegistry) { + if self.handled_at.is_some() && self.exceeded_debounce_duration() { + if let Some((variable, value)) = &self.reset_to { + variables.write(variable, *value); + } + + self.handled_at = None; + } + } +} + +#[derive(Clone, Copy, Default)] +/// Configurable options for event to variable handling. +pub struct EventToVariableOptions { + mask: bool, + leading_debounce_duration: Option, + reset_to: Option, +} + +impl EventToVariableOptions { + /// Masks the event, causing the simulator to ignore it, and only this module to receive it. + pub fn mask(mut self) -> Self { + self.mask = true; + self + } + + /// Apply a leading debounce to the event handling. Leading debounce immediately executes + /// the action associated with the event, but ignores any subsequent event until no event + /// triggered for the given duration. + /// + /// This is useful to deal with poor MSFS event handling, e.g. events being triggered + /// repeatedly despite only one press occurring. + pub fn leading_debounce(mut self, duration: Duration) -> Self { + self.leading_debounce_duration = Some(duration); + self + } + + /// Sets the value to which the variable should be reset after the event occurred and any + /// debounce duration has passed. + pub fn afterwards_reset_to(mut self, value: f64) -> Self { + self.reset_to = Some(value); + self + } +} + +/// Declares how to map the given event to a variable value. +pub enum EventToVariableMapping { + /// When the event occurs, sets the variable to the given value. + Value(f64), + + /// Maps the event data from a u32 to an f64 without any further processing. + EventDataRaw, + + /// Maps the event data from a 32k position to an f64. + EventData32kPosition, + + /// When the event occurs, calls the function with event data and sets + /// the variable to the returned value. + EventDataToValue(fn(u32) -> f64), + + /// When the event occurs, calls the function with the current variable value and + /// sets the variable to the returned value. + CurrentValueToValue(fn(f64) -> f64), + + /// When the event occurs, calls the function with event data and the current + /// variable value and sets the variable to the returned value. + EventDataAndCurrentValueToValue(fn(u32, f64) -> f64), + + /// Converts the event occurrence to a value which increases and decreases + /// by the given factors. + SmoothPress(f64, f64), +} + +#[enum_dispatch] +enum MessageHandler { + EventToVariable, +} + +#[enum_dispatch(MessageHandler)] +trait HandleMessages { + fn handle(&mut self, message: &SimConnectRecv, variables: &mut MsfsVariableRegistry) -> bool; + fn pre_tick(&mut self, variables: &mut MsfsVariableRegistry, delta: Duration); + fn post_tick(&mut self, variables: &mut MsfsVariableRegistry); +} + +struct EventToVariable { + event_id: u32, + event_handled_before_tick: bool, + target: VariableIdentifier, + mapping: EventToVariableMapping, + debounce: Debounce, +} + +impl EventToVariable { + fn new( + sim_connect: &mut SimConnect, + event_name: &str, + mapping: EventToVariableMapping, + target: VariableIdentifier, + options: EventToVariableOptions, + ) -> Result> { + let reset_to = options.reset_to.map(|value| (target, value)); + let debounce = if let Some(duration) = options.leading_debounce_duration { + LeadingDebounce::new(duration, reset_to).into() + } else { + NoDebounce::new(reset_to).into() + }; + + Ok(Self { + event_id: sim_connect.map_client_event_to_sim_event(event_name, options.mask)?, + event_handled_before_tick: false, + target, + mapping, + debounce, + }) + } + + fn map_to_value( + &self, + e: &msfs::sys::SIMCONNECT_RECV_EVENT, + variables: &mut MsfsVariableRegistry, + ) -> f64 { + match self.mapping { + EventToVariableMapping::Value(value) => value, + EventToVariableMapping::EventDataRaw => e.data() as f64, + EventToVariableMapping::EventData32kPosition => sim_connect_32k_pos_to_f64(e.data()), + EventToVariableMapping::EventDataToValue(func) => func(e.data()), + EventToVariableMapping::CurrentValueToValue(func) => { + func(variables.read(&self.target).unwrap_or(0.)) + } + EventToVariableMapping::EventDataAndCurrentValueToValue(func) => { + func(e.data(), variables.read(&self.target).unwrap_or(0.)) + } + EventToVariableMapping::SmoothPress(..) => variables.read(&self.target).unwrap_or(0.), + } + } + + fn adjust_smooth_pressed_value( + &mut self, + delta: Duration, + variables: &mut MsfsVariableRegistry, + ) { + if let EventToVariableMapping::SmoothPress(press_factor, release_factor) = self.mapping { + let mut value = variables.read(&self.target).unwrap_or(0.); + if self.event_handled_before_tick { + value += delta.as_secs_f64() * press_factor; + } else { + value -= delta.as_secs_f64() * release_factor; + } + + variables.write(&self.target, value.min(1.).max(0.)); + } + } +} + +impl HandleMessages for EventToVariable { + fn handle(&mut self, message: &SimConnectRecv, variables: &mut MsfsVariableRegistry) -> bool { + match message { + SimConnectRecv::Event(e) if e.id() == self.event_id => { + if self.debounce.should_handle() { + let mapped_value = self.map_to_value(e, variables); + variables.write(&self.target, mapped_value); + + self.debounce.notify_handled(); + self.event_handled_before_tick = true; + } + + true + } + _ => false, + } + } + + fn pre_tick(&mut self, variables: &mut MsfsVariableRegistry, delta: Duration) { + self.adjust_smooth_pressed_value(delta, variables); + } + + fn post_tick(&mut self, variables: &mut MsfsVariableRegistry) { + self.debounce.post_tick(variables); + self.event_handled_before_tick = false; + } +} + +#[derive(Clone, Copy)] +/// Declares how to map the given variable value to an event value. +pub enum VariableToEventMapping { + /// Maps the variable from an f64 to a u32 without any further processing. + EventDataRaw, + + /// Maps the variable from an f64 to a 32k position. + EventData32kPosition, +} + +/// Declares when to write the variable to the event. +#[derive(Clone, Copy)] +pub enum VariableToEventWriteOn { + /// Writes the variable to the event after every tick. + EveryTick, + + /// Writes the variable to the event when the variable's value has changed. + Change, +} + +struct ToEvent { + input: VariableIdentifier, + mapping: VariableToEventMapping, + write_on: VariableToEventWriteOn, + event_id: u32, + last_written_value: Option, +} + +impl ToEvent { + fn new( + sim_connect: &mut SimConnect, + input: VariableIdentifier, + mapping: VariableToEventMapping, + write_on: VariableToEventWriteOn, + event_name: &str, + ) -> Result> { + Ok(Self { + input, + mapping, + write_on, + event_id: sim_connect.map_client_event_to_sim_event(event_name, false)?, + last_written_value: None, + }) + } + + fn new_with_event_id( + input: VariableIdentifier, + mapping: VariableToEventMapping, + write_on: VariableToEventWriteOn, + event_id: u32, + ) -> Self { + Self { + input, + mapping, + write_on, + event_id, + last_written_value: None, + } + } +} + +impl ExecutableVariableAction for ToEvent { + fn execute( + &mut self, + sim_connect: &mut SimConnect, + variables: &mut MsfsVariableRegistry, + ) -> Result<(), Box> { + let value = variables.read(&self.input).unwrap_or(0.); + let should_write = match self.write_on { + VariableToEventWriteOn::EveryTick => true, + VariableToEventWriteOn::Change => match self.last_written_value { + Some(last_written_value) => value != last_written_value, + None => true, + }, + }; + + if should_write { + sim_connect.transmit_client_event( + SIMCONNECT_OBJECT_ID_USER, + self.event_id, + match self.mapping { + VariableToEventMapping::EventDataRaw => value as u32, + VariableToEventMapping::EventData32kPosition => { + f64_to_sim_connect_32k_pos(value) + } + }, + )?; + self.last_written_value = Some(value); + } + + Ok(()) + } +} diff --git a/src/systems/systems_wasm/src/electrical.rs b/src/systems/systems_wasm/src/electrical.rs index 6b57c679525..1fe65de5c0e 100644 --- a/src/systems/systems_wasm/src/electrical.rs +++ b/src/systems/systems_wasm/src/electrical.rs @@ -1,7 +1,7 @@ use fxhash::FxHashMap; use std::error::Error; -use crate::SimulatorAspect; +use crate::Aspect; use msfs::legacy::execute_calculator_code; use msfs::legacy::AircraftVariable; use systems::shared::to_bool; @@ -24,7 +24,7 @@ impl MsfsElectricalBuses { .insert(identifier, ElectricalBusConnection::new(from, to)); } } -impl SimulatorAspect for MsfsElectricalBuses { +impl Aspect for MsfsElectricalBuses { fn write(&mut self, identifier: &VariableIdentifier, value: f64) -> bool { match self.connections.get_mut(identifier) { Some(connection) => connection.update(value), @@ -100,7 +100,7 @@ impl MsfsAuxiliaryPowerUnit { execute_calculator_code::<()>("1 (>K:APU_OFF_SWITCH, Number)"); } } -impl SimulatorAspect for MsfsAuxiliaryPowerUnit { +impl Aspect for MsfsAuxiliaryPowerUnit { fn write(&mut self, identifier: &VariableIdentifier, value: f64) -> bool { if identifier == &self.is_available_id { let is_available = to_bool(value); diff --git a/src/systems/systems_wasm/src/lib.rs b/src/systems/systems_wasm/src/lib.rs index 6daabeb0c86..503aa448e3c 100644 --- a/src/systems/systems_wasm/src/lib.rs +++ b/src/systems/systems_wasm/src/lib.rs @@ -1,16 +1,21 @@ #![cfg(any(target_arch = "wasm32", doc))] +#[macro_use] +pub mod aspects; mod electrical; mod failures; -use std::{error::Error, time::Duration}; - +use crate::aspects::MsfsAspectBuilder; +use electrical::{MsfsAuxiliaryPowerUnit, MsfsElectricalBuses}; +use failures::Failures; use fxhash::FxHashMap; use msfs::{ legacy::{AircraftVariable, NamedVariable}, sim_connect::{data_definition, Period, SimConnect, SimConnectRecv, SIMCONNECT_OBJECT_ID_USER}, MSFSEvent, }; - +use std::fmt::{Display, Formatter}; +use std::{error::Error, time::Duration}; +use systems::simulation::InitContext; use systems::{ failures::FailureType, simulation::{ @@ -18,12 +23,9 @@ use systems::{ }, }; -use electrical::{MsfsAuxiliaryPowerUnit, MsfsElectricalBuses}; -use failures::Failures; -use systems::simulation::InitContext; - -/// An aspect to inject into events in the simulation. -pub trait SimulatorAspect { +/// A concern that should be handled by the bridging layer. Examples are +/// the handling of events which move flaps up and down, triggering of brakes, etc. +pub trait Aspect { /// Attempts to read data with the given identifier. /// Returns `Some` when reading was successful, `None` otherwise. fn read(&mut self, _identifier: &VariableIdentifier) -> Option { @@ -40,30 +42,38 @@ pub trait SimulatorAspect { false } - /// Attempts to handle the given SimConnect message, returning true + /// Attempts to handle the given message, returning true /// when the message was handled and false otherwise. - fn handle_message(&mut self, _message: &SimConnectRecv) -> bool { + fn handle_message( + &mut self, + _message: &SimConnectRecv, + _variables: &mut MsfsVariableRegistry, + ) -> bool { false } /// Executes before a simulation tick runs. - fn pre_tick(&mut self, _delta: Duration) {} + fn pre_tick( + &mut self, + _variables: &mut MsfsVariableRegistry, + _sim_connect: &mut SimConnect, + _delta: Duration, + ) -> Result<(), Box> { + Ok(()) + } /// Executes after a simulation tick ran. - fn post_tick(&mut self, _sim_connect: &mut SimConnect) -> Result<(), Box> { + fn post_tick( + &mut self, + _variables: &mut MsfsVariableRegistry, + _sim_connect: &mut SimConnect, + ) -> Result<(), Box> { Ok(()) } } -pub trait MsfsAspectCtor { - fn new( - registry: &mut MsfsVariableRegistry, - sim_connect: &mut SimConnect, - ) -> Result> - where - Self: Sized; -} - +/// Type used to configure and build a simulation and a handler which acts as a bridging layer +/// between the simulation and Microsoft Flight Simulator. pub struct MsfsSimulationBuilder<'a, 'b> { variable_registry: Option, key_prefix: String, @@ -71,16 +81,16 @@ pub struct MsfsSimulationBuilder<'a, 'b> { sim_connect: &'a mut SimConnect<'b>, apu: Option, failures: Option, - additional_aspects: Vec>, + additional_aspects: Vec>, } impl<'a, 'b> MsfsSimulationBuilder<'a, 'b> { const MSFS_INFINITELY_POWERED_BUS_IDENTIFIER: usize = 1; - pub fn new(key_prefix: String, sim_connect: &'a mut SimConnect<'b>) -> Self { + pub fn new(key_prefix: &str, sim_connect: &'a mut SimConnect<'b>) -> Self { Self { - variable_registry: Some(MsfsVariableRegistry::new(key_prefix.clone())), - key_prefix, + variable_registry: Some(MsfsVariableRegistry::new(key_prefix.into())), + key_prefix: key_prefix.into(), electrical_buses: Some(Default::default()), sim_connect, apu: None, @@ -93,8 +103,7 @@ impl<'a, 'b> MsfsSimulationBuilder<'a, 'b> { mut self, aircraft_ctor_fn: U, ) -> Result<(Simulation, MsfsHandler), Box> { - let mut aspects: Vec> = - vec![Box::new(self.electrical_buses.unwrap())]; + let mut aspects: Vec> = vec![Box::new(self.electrical_buses.unwrap())]; if self.apu.is_some() { aspects.push(Box::new(self.apu.unwrap())); } @@ -103,21 +112,23 @@ impl<'a, 'b> MsfsSimulationBuilder<'a, 'b> { let mut registry = self.variable_registry.unwrap(); let simulation = Simulation::new(aircraft_ctor_fn, &mut registry); - aspects.push(Box::new(registry)); Ok(( simulation, - MsfsHandler::new(aspects, self.failures, self.sim_connect)?, + MsfsHandler::new(registry, aspects, self.failures, self.sim_connect)?, )) } - pub fn with( + /// Adds an aspect. An aspect is a concern that should be handled by the bridging layer. + /// The function passed to this method is used to configure the aspect. + pub fn with_aspect Result<(), Box>>( mut self, + builder_func: T, ) -> Result> { - if let Some(registry) = &mut self.variable_registry { - self.additional_aspects - .push(Box::new(T::new(registry, self.sim_connect)?)); - } + let variable_registry = &mut self.variable_registry.as_mut().unwrap(); + let mut builder = MsfsAspectBuilder::new(&mut self.sim_connect, variable_registry); + (builder_func)(&mut builder)?; + self.additional_aspects.push(Box::new(builder.build())); Ok(self) } @@ -146,13 +157,13 @@ impl<'a, 'b> MsfsSimulationBuilder<'a, 'b> { pub fn with_auxiliary_power_unit( mut self, - is_available_variable_name: String, + is_available_variable_name: &str, fuel_valve_number: u8, ) -> Result> { if let Some(registry) = &mut self.variable_registry { self.apu = Some(MsfsAuxiliaryPowerUnit::new( registry, - is_available_variable_name, + is_available_variable_name.into(), fuel_valve_number, )?); } @@ -181,45 +192,33 @@ impl<'a, 'b> MsfsSimulationBuilder<'a, 'b> { index: usize, ) -> Result> { if let Some(registry) = &mut self.variable_registry { - registry.add_aircraft_variable(name, units, index)?; - } - - Ok(self) - } - - pub fn provides_aircraft_variable_with_additional_names( - mut self, - name: &str, - units: &str, - index: usize, - additional_names: Vec, - ) -> Result> { - if let Some(registry) = &mut self.variable_registry { - registry.add_aircraft_variable_with_additional_names( - name, - units, + registry.register(&Variable::Aircraft( + name.to_owned(), + units.to_owned(), index, - Some(additional_names), - )?; + )); } Ok(self) } } -/// Used to orchestrate the simulation combined with Microsoft Flight Simulator. +/// Used to bridge between the simulation and Microsoft Flight Simulator. pub struct MsfsHandler { - aspects: Vec>, + variables: Option, + aspects: Vec>, failures: Option, time: Time, } impl MsfsHandler { fn new( - aspects: Vec>, + variables: MsfsVariableRegistry, + aspects: Vec>, failures: Option, sim_connect: &mut SimConnect, ) -> Result> { Ok(Self { + variables: Some(variables), aspects, failures, time: Time::new(sim_connect)?, @@ -236,7 +235,7 @@ impl MsfsHandler { MSFSEvent::PreDraw(_) => { if !self.time.is_pausing() { let delta_time = self.time.take(); - self.pre_tick(delta_time); + self.pre_tick(sim_connect, delta_time)?; if let Some(failures) = &self.failures { Self::read_failures_into_simulation(failures, simulation); } @@ -260,22 +259,43 @@ impl MsfsHandler { } fn handle_message(&mut self, message: &SimConnectRecv) { - for aspect in self.aspects.iter_mut() { - if aspect.handle_message(message) { - break; + if let Some(mut variables) = self.variables.take() { + for aspect in self.aspects.iter_mut() { + if aspect.handle_message(message, &mut variables) { + break; + } } + + self.variables = Some(variables); } } - fn pre_tick(&mut self, delta: Duration) { - self.aspects.iter_mut().for_each(|aspect| { - aspect.pre_tick(delta); - }); + fn pre_tick( + &mut self, + sim_connect: &mut SimConnect, + delta: Duration, + ) -> Result<(), Box> { + if let Some(mut variables) = self.variables.take() { + let result = self + .aspects + .iter_mut() + .try_for_each(|aspect| aspect.pre_tick(&mut variables, sim_connect, delta)); + + self.variables = Some(variables); + + result + } else { + Ok(()) + } } fn post_tick(&mut self, sim_connect: &mut SimConnect) -> Result<(), Box> { - for aspect in self.aspects.iter_mut() { - aspect.post_tick(sim_connect)?; + if let Some(mut variables) = self.variables.take() { + for aspect in self.aspects.iter_mut() { + aspect.post_tick(&mut variables, sim_connect)?; + } + + self.variables = Some(variables); } Ok(()) @@ -299,107 +319,265 @@ impl SimulatorReaderWriter for MsfsHandler { self.aspects .iter_mut() .find_map(|aspect| aspect.read(identifier)) - .unwrap_or(0.) + .unwrap_or_else(|| { + self.variables + .as_ref() + .map(|registry| registry.read(identifier).unwrap_or(0.)) + .unwrap_or(0.) + }) } fn write(&mut self, identifier: &VariableIdentifier, value: f64) { for aspect in self.aspects.iter_mut() { if aspect.write(identifier, value) { - break; + return; } } + + if let Some(variable_registry) = &mut self.variables { + variable_registry.write(identifier, value); + } + } +} + +/// Declares a variable of a given type with a given name. +#[derive(Clone)] +pub enum Variable { + /// An aircraft variable accessible within the aspect, simulation and simulator. + Aircraft(String, String, usize), + + /// A named variable accessible within the aspect, simulation and simulator. + Named(String), + + /// A variable accessible within all aspects and the simulation. + /// + /// Note that even though aspect variables are accessible from all aspects, no assumptions + /// should be made about their update order outside of a single aspect. Thus it is best not + /// to use the same aspect variable within different aspects. + Aspect(String), +} + +impl Display for Variable { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let name = match self { + Self::Aircraft(name, _, index) => { + format!("Aircraft({})", Self::indexed_name(name, *index)) + } + Self::Named(name, ..) => format!("Named({})", name), + Self::Aspect(name, ..) => format!("Aspect({})", name), + }; + + write!(f, "{}", name) + } +} + +impl Variable { + pub fn aircraft(name: &str, units: &str, index: usize) -> Self { + Self::Aircraft(name.into(), units.into(), index) + } + + pub fn named(name: &str) -> Self { + Self::Named(name.into()) + } + + pub fn aspect(name: &str) -> Self { + Self::Aspect(name.into()) + } + + /// Provides the name that should be used for storing and looking up a [VariableIdentifier]. + fn lookup_name(&self) -> String { + match self { + Self::Aircraft(name, _, index, ..) => Self::indexed_name(name, *index), + Self::Named(name, ..) | Self::Aspect(name, ..) => name.into(), + } + } + + fn add_prefix(&mut self, prefix: &str) { + match self { + Self::Aircraft(name, ..) | Self::Named(name, ..) | Self::Aspect(name, ..) => { + *name = format!("{}{}", prefix, name); + } + } + } + + fn indexed_name(name: &str, index: usize) -> String { + if index > 0 { + format!("{}:{}", name, index) + } else { + name.into() + } + } +} + +impl From<&Variable> for VariableValue { + fn from(value: &Variable) -> Self { + match value { + Variable::Aircraft(name, units, index, ..) => { + let index = *index; + VariableValue::Aircraft(match AircraftVariable::from(&name, units, index) { + Ok(aircraft_variable) => aircraft_variable, + Err(error) => panic!( + "Error while trying to create aircraft variable named '{}': {}", + name, error + ), + }) + } + Variable::Named(name, ..) => VariableValue::Named(NamedVariable::from(name)), + Variable::Aspect(..) => VariableValue::Aspect(0.), + } + } +} + +impl From<&Variable> for VariableType { + fn from(value: &Variable) -> Self { + match value { + Variable::Aircraft(..) => Self::Aircraft, + Variable::Named(..) => Self::Named, + Variable::Aspect(..) => Self::Aspect, + } + } +} + +#[derive(Debug, Eq, Hash, PartialEq)] +pub enum VariableType { + Aircraft = 0, + Named = 1, + Aspect = 2, +} + +impl From for u8 { + fn from(value: VariableType) -> Self { + value as u8 + } +} + +impl From for VariableType { + fn from(value: u8) -> Self { + match value { + 0 => Self::Aircraft, + 1 => Self::Named, + 2 => Self::Aspect, + _ => panic!("Cannot convert {} to identifier type", value), + } + } +} + +impl From<&VariableIdentifier> for VariableType { + fn from(value: &VariableIdentifier) -> Self { + value.identifier_type().into() + } +} + +pub enum VariableValue { + Aircraft(AircraftVariable), + Named(NamedVariable), + Aspect(f64), +} + +impl VariableValue { + fn read(&self) -> f64 { + match self { + Self::Aircraft(underlying) => underlying.get(), + Self::Named(underlying) => underlying.get_value(), + Self::Aspect(underlying) => *underlying, + } + } + + fn write(&mut self, value: f64) { + match self { + Self::Aircraft(_) => panic!("Cannot write to an aircraft variable."), + Self::Named(underlying) => underlying.set_value(value), + Self::Aspect(underlying) => *underlying = value, + } } } pub struct MsfsVariableRegistry { - name_to_identifier: FxHashMap, - aircraft_variables: Vec, - named_variables: Vec, named_variable_prefix: String, - next_aircraft_variable_identifier: VariableIdentifier, - next_named_variable_identifier: VariableIdentifier, + name_to_identifier: FxHashMap, + next_variable_identifier: FxHashMap, + variables: FxHashMap, } impl MsfsVariableRegistry { - const AIRCRAFT_VARIABLE_IDENTIFIER_TYPE: u8 = 0; - const NAMED_VARIABLE_IDENTIFIER_TYPE: u8 = 1; - pub fn new(named_variable_prefix: String) -> Self { Self { - name_to_identifier: FxHashMap::default(), - aircraft_variables: Default::default(), - named_variables: Default::default(), named_variable_prefix, - next_aircraft_variable_identifier: VariableIdentifier::new( - Self::AIRCRAFT_VARIABLE_IDENTIFIER_TYPE, - ), - next_named_variable_identifier: VariableIdentifier::new( - Self::NAMED_VARIABLE_IDENTIFIER_TYPE, - ), + name_to_identifier: FxHashMap::default(), + next_variable_identifier: FxHashMap::default(), + variables: FxHashMap::default(), } } - /// Add an aircraft variable definition. Once added, the aircraft variable + /// Registers a variable definition. Once added, the variable /// can be read through the `MsfsVariableRegistry.read` function. - pub fn add_aircraft_variable( - &mut self, - name: &str, - units: &str, - index: usize, - ) -> Result<(), Box> { - self.add_aircraft_variable_with_additional_names(name, units, index, None) + pub fn register(&mut self, variable: &Variable) -> VariableIdentifier { + let identifier = self.get_identifier_or_create_variable(variable); + + let registered_type: VariableType = (&identifier).into(); + let target_type: VariableType = variable.into(); + if registered_type != target_type { + eprintln!("Attempted to re-register a variable \"{}\" which was previously registered with type {:?}.", + variable, registered_type); + } + + identifier } - /// Add an aircraft variable definition. Once added, the aircraft variable - /// can be read through the `MsfsVariableRegistry.read` function. - /// - /// The additional names map to the same variable. - pub fn add_aircraft_variable_with_additional_names( - &mut self, - name: &str, - units: &str, - index: usize, - additional_names: Option>, - ) -> Result<(), Box> { - match AircraftVariable::from(&name, units, index) { - Ok(var) => { - let name = if index > 0 { - format!("{}:{}", name, index) - } else { - name.to_owned() - }; - - let identifier = self.next_aircraft_variable_identifier; - - self.aircraft_variables - .insert(identifier.identifier_index(), var); - self.name_to_identifier.insert(name, identifier); - - if let Some(additional_names) = additional_names { - additional_names.into_iter().for_each(|el| { - self.name_to_identifier.insert(el, identifier); - }); + pub fn register_many(&mut self, variables: &[Variable]) -> Vec { + variables + .into_iter() + .map(|variable| self.register(variable)) + .collect() + } + + fn get_identifier_or_create_variable(&mut self, variable: &Variable) -> VariableIdentifier { + match self.name_to_identifier.get(&variable.lookup_name()) { + Some(identifier) => *identifier, + None => { + let identifier = *self + .next_variable_identifier + .entry(variable.into()) + .or_insert_with(|| VariableIdentifier::new::(variable.into())); + + self.next_variable_identifier + .insert(variable.into(), identifier.next()); + + self.name_to_identifier + .insert(variable.lookup_name(), identifier); + + let mut variable = variable.clone(); + if matches!(variable, Variable::Named(..)) { + variable.add_prefix(&self.named_variable_prefix); } - self.next_aircraft_variable_identifier = identifier.next(); + let value: VariableValue = (&variable).into(); + self.variables.insert(identifier, value); - Ok(()) + identifier } - Err(x) => Err(x), } } - fn add_named_variable(&mut self, name: String) -> VariableIdentifier { - let identifier = self.next_named_variable_identifier; - self.named_variables.insert( - identifier.identifier_index(), - NamedVariable::from(&format!("{}{}", self.named_variable_prefix, name)), - ); - self.name_to_identifier.insert(name, identifier); + fn read(&self, identifier: &VariableIdentifier) -> Option { + match self.variables.get(identifier) { + Some(variable_value) => Some(variable_value.read()), + None => None, + } + } - self.next_named_variable_identifier = identifier.next(); + fn read_many(&self, identifiers: &[VariableIdentifier]) -> Vec> { + identifiers + .iter() + .map(|identifier| self.read(identifier)) + .collect() + } - identifier + fn write(&mut self, identifier: &VariableIdentifier, value: f64) { + match self.variables.get_mut(identifier) { + Some(variable_value) => variable_value.write(value), + None => (), + } } } @@ -407,31 +585,9 @@ impl VariableRegistry for MsfsVariableRegistry { fn get(&mut self, name: String) -> VariableIdentifier { match self.name_to_identifier.get(&name) { Some(identifier) => *identifier, - None => self.add_named_variable(name), - } - } -} - -impl SimulatorAspect for MsfsVariableRegistry { - fn read(&mut self, identifier: &VariableIdentifier) -> Option { - match identifier.identifier_type() { - Self::AIRCRAFT_VARIABLE_IDENTIFIER_TYPE => { - Some(self.aircraft_variables[identifier.identifier_index()].get()) - } - Self::NAMED_VARIABLE_IDENTIFIER_TYPE => { - Some(self.named_variables[identifier.identifier_index()].get_value()) - } - _ => None, - } - } - - fn write(&mut self, identifier: &VariableIdentifier, value: f64) -> bool { - match identifier.identifier_type() { - Self::NAMED_VARIABLE_IDENTIFIER_TYPE => { - self.named_variables[identifier.identifier_index()].set_value(value); - true - } - _ => false, + // By the time this function is called, only named variables are to be created. + // Other variable types have been instantiated through the MsfsSimulationBuilder. + None => self.register(&Variable::Named(name)), } } }