# # TOWING NASAL CODE # # original version by D-NXKT up to version 30.12.2014 # updates by Benedikt Wolf and D-NXKT, version 01/2023 # # # Purpose of this routine: # ------------------------ # # - Create visible winch- and towropes for gliders and towplanes # - Support of aerotowing and winch for JSBSim-aircraft (glider and towplanes) # # This routine is very similar to /FDM/YASim/Hitch.cpp # Aerotowing is fully compatible to the YASim functionality. # This means that YASim-gliders could be towed by JSBSim-aircraft and vice versa. # Setup-instructions with copy and paste examples are given below: # # # Setup of visible winch/towropes for Yasim-aircraft: # ---------------------------------------------------- # # YASim-aircraft with winch/aerotowing functionality should work out of the box. # Optional you can customize the rope-diameter by adding the following to "your_aircraft-set.xml": # # # # # 10 # # # # # 20 # # # # # # That's all! # # # # Support of aerotowing and winch for JSBSim-aircraft (glider and towplanes): # ---------------------------------------------------------------------------- # # 1. Define a hitch in the JSBSim-File. Coordinates according to JSBSims structural frame of reference # (x points to the tail, y points to the right wing, z points upwards). # Unit must be "LBS", frame must be "BODY". The force name is arbitrary. # # # # # 3.65 # 0.0 # -0.12 # # # 0.0 # 0.0 # 0.0 # # # # 2. Define controls for aerotowing and winch. # Add the following key bindings in "yourAircraft-set.xml": # # # # # Ctrl-o # Find aircraft for aerotow # # nasal # # # # # # o # Lock aerotow-hook # # nasal # # # # # # O # Open aerotow-hook # # nasal # # # # # # Ctrl-w # Place Winch and hook in # # nasal # # # # # # w # Start winch # # nasal # # # # # # W # Open winch-hook # # nasal # # # # # # # # For towplanes only "key n=79" (Open aerotow-hook) is required! # 3. Set mandatory properties: # # # # hitch # false # 0.0 # # # # hitch # # # # # # "force_name_jsbsim" must be the external force name in JSBSim. # "force-is-calculated-by-other" should be "false" for gliders and "true" for tow planes. # "mp-auto-connect-period" is only needed for tow planes and should be "1". # # IMPORTANT: # The hitch location is stored twice in the property tree (for tow force and for rope animation). # This is necessary to keep the towrope animation compatible to YASim-aircraft. # The hitch location for the tow force is stored in "fdm/jsbsim/external_reactions/hitch/location-x(yz)-in" and for the # animated towrope in "sim/hitches/aerotow(winch)/local-pos-x(yz)". # By default only values for the tow force location have to be defined. The values for the towrope location are set # automatically (decoupled-force-and-rope-locations is "false" by default). # It is feasible to use different locations for the force and rope. In order to do this, you have to set # "decoupled-force-and-rope-locations" to "true" and provide values for "sim/hitches/aerotow(winch)/local-pos-x(yz)". # Note that the frame of reference is different. Here the coordinates for the "YASim-System" are needed # (x points to the nose, y points to the left wing, z points upwards). # 4. Set optional properties: # # Only aircraft-specific properties should be set by the aircraft. All other options can be adjusted by the # winch GUI dialog and are stored by this script itself (TODO) # # Aircraft-specific properties are # * weak link (aerotow/winch) # sim/hitches/aerotow/tow/break-fource [N] # sim/hitches/winch/tow/break-force [N] # * typical tow speed (winch) # sim/hitches/winch/typical-tow-speed-kph [kph] # * automatic release angle (winch) # sim/hitches/winch/automatic-release-angle-deg [deg] # # # # # # 6000 # # # # 70. # 100 # 5000 # # 10000 # # # # # That's it! ################################################## general info ############################################ # # 3 different types of towplanes could exist: AI-plane, MP-plane without interaction, MP-plane with interaction. # AI-planes are identified by the node "ai/models/aircraft/" or "ai/models/dragger". # MP-planes (interactice/non-interactive) are identified by the existence of node "ai/models/multiplayer". # Interactive MP-plane: variables in node "ai/models/multiplayer/sim/hitches/" are updated. # Non-interactive MP-plane: variables are not updated (values are either not defined or have "wrong" values # from a former owner of this node. # # The following properties are transmitted in multiplayer: # "sim/hitches/aerotow/tow/elastic-constant" # "sim/hitches/aerotow/tow/weight-per-m-kg-m" # "sim/hitches/aerotow/tow/dist" # "sim/hitches/aerotow/tow/connected-to-property-node" # "sim/hitches/aerotow/tow/connected-to-ai-or-mp-callsign" # "sim/hitches/aerotow/tow/break-force" # "sim/hitches/aerotow/tow/end-force-x" # "sim/hitches/aerotow/tow/end-force-y" # "sim/hitches/aerotow/tow/end-force-z" # "sim/hitches/aerotow/is-slave" # "sim/hitches/aerotow/speed-in-tow-direction" # "sim/hitches/aerotow/open", open); # "sim/hitches/aerotow/local-pos-x" # "sim/hitches/aerotow/local-pos-y" # "sim/hitches/aerotow/local-pos-z" # ############################################################################################################## # ###################################################################################################################### # check, if towing support makes sense # ###################################################################################################################### # Check if node "sim/hitches" is defined. If not, return! if (props.globals.getNode("sim/hitches") == nil ) return; print("towing is active!"); # ###################################################################################################################### # set defaults / initialize at startup # ###################################################################################################################### # set defaults for properties that are NOT already defined var fdm = getprop("/sim/flight-model") or ""; if ( !(fdm == "jsb" or fdm == "yasim") ){ print("Unsupported FDM for towing"); return; } var check_or_create = func ( name, value, type ) { if ( getprop( name ) == nil ){ return props.globals.initNode(name, value, type); } else { return props.globals.getNode(name); } } # Load stored settings var config_file = getprop("/sim/fg-home") ~ "/Export/hitch-config.xml"; var load_prop = func( property, cnfg_p ){ if( cnfg_p != nil ){ property.setValue( cnfg_p.getValue() ); } } var config = { aerotow: { elastic_constant: props.globals.initNode("sim/hitches/aerotow/tow/elastic-constant", 9111.0, "DOUBLE"), # saved in config weight_per_m_kg_m: props.globals.initNode("sim/hitches/aerotow/tow/weight-per-m-kg-m", 0.35, "DOUBLE"), # saved in config length: props.globals.initNode("sim/hitches/aerotow/tow/length", 60.0, "DOUBLE"), # saved in config path_to_model: props.globals.initNode("sim/hitches/aerotow/rope/path_to_model", "Models/Aircraft/towropes.xml", "STRING"), # saved in config rope_diameter_mm: props.globals.initNode("sim/hitches/aerotow/rope/rope-diameter-mm", 20.0, "DOUBLE"), # saved in config }, winch: { elastic_constant: props.globals.initNode("sim/hitches/winch/tow/elastic-constant", 40001.0, "DOUBLE"), # saved in config weight_per_m_kg_m: props.globals.initNode("sim/hitches/winch/tow/weight-per-m-kg-m", 0.1, "DOUBLE"), # saved in config initial_tow_length: props.globals.initNode("sim/hitches/winch/winch/initial-tow-length-m", 1000.0, "DOUBLE"), # saved in config path_to_model: props.globals.initNode("sim/hitches/winch/rope/path_to_model", "Models/Aircraft/towropes.xml", "STRING"), # saved in config rope_diameter_mm: props.globals.initNode("sim/hitches/winch/rope/rope-diameter-mm", 20.0, "DOUBLE"), # saved in config rope_breakage: { height: props.globals.initNode("sim/hitches/winch/breakage/height", 100.0, "DOUBLE"), # saved in config random: props.globals.initNode("sim/hitches/winch/breakage/random", 0, "BOOL"), # saved in config }, loss_of_power: { height: props.globals.initNode("sim/hitches/winch/loss-of-power/height", 100.0, "DOUBLE"), # saved in config random: props.globals.initNode("sim/hitches/winch/loss-of-power/random", 0, "BOOL"), # saved in config }, max_tow_length_m: props.globals.initNode("sim/hitches/winch/winch/max-tow-length-m", 1500.0, "DOUBLE"), # saved in config max_spool_speed: props.globals.initNode("sim/hitches/winch/winch/max-spool-speed-m-s", 40.0, "DOUBLE"), # saved in config force_acceleration: props.globals.initNode("sim/hitches/winch/winch/force-acceleration-N-s", 1000.0, "DOUBLE"), # saved in config spool_acceleration: props.globals.initNode("sim/hitches/winch/winch/spool-acceleration-m-s-s", 8.0, "DOUBLE"), # saved in config max_unspool_speed: props.globals.initNode("sim/hitches/winch/winch/max-unspool-speed-m-s", 40.0, "DOUBLE"), # saved in config max_force: props.globals.initNode("sim/hitches/winch/winch/max-force-N", 10000.0, "DOUBLE"), # saved in config max_power: props.globals.initNode("sim/hitches/winch/winch/max-power-kW", 123.0, "DOUBLE"), # saved in config magic_constant: props.globals.initNode("sim/hitches/winch/winch/magic-constant", 500.0, "DOUBLE"), # saved in config messages: { launch_signaller: props.globals.initNode("/sim/hitches/winch/messages/launch-signaller", 1, "BOOL"), # saved in config winch_driver: props.globals.initNode("/sim/hitches/winch/messages/winch-driver", 1, "BOOL"), # saved in config pilot: props.globals.initNode("/sim/hitches/winch/messages/pilot", 1, "BOOL"), # saved in config remote_ac: props.globals.initNode("/sim/hitches/winch/messages/remote-ac", 1, "BOOL"), # saved in config }, }, }; var write_config = func { var c = props.Node.new(); var a = c.addChild("aerotow"); var w = c.addChild("winch"); a.initNode("elastic-constant").setDoubleValue( config.aerotow.elastic_constant.getDoubleValue() ); a.initNode("weight-per-m-kg-m").setDoubleValue( config.aerotow.weight_per_m_kg_m.getDoubleValue() ); a.initNode("length").setDoubleValue( config.aerotow.length.getDoubleValue() ); a.initNode("path_to_model","","STRING").setValue( config.aerotow.path_to_model.getValue() ); a.initNode("rope-diameter-mm").setDoubleValue( config.aerotow.rope_diameter_mm.getDoubleValue() ); w.initNode("elastic-constant").setDoubleValue( config.winch.elastic_constant.getDoubleValue() ); w.initNode("weight-per-m-kg-m").setDoubleValue( config.winch.weight_per_m_kg_m.getDoubleValue() ); w.initNode("initial-tow-length-m").setDoubleValue( config.winch.initial_tow_length.getDoubleValue() ); w.initNode("path_to_model","","STRING").setValue( config.winch.path_to_model.getValue() ); w.initNode("rope-diameter-mm").setDoubleValue( config.winch.rope_diameter_mm.getDoubleValue() ); w.initNode("breakage/height").setDoubleValue(config.winch.rope_breakage.height.getDoubleValue() ); w.initNode("breakage/random",0,"BOOL").setBoolValue(config.winch.rope_breakage.random.getBoolValue() ); w.initNode("loss-of-power/height").setDoubleValue(config.winch.loss_of_power.height.getDoubleValue() ); w.initNode("loss-of-power/random",0,"BOOL").setBoolValue(config.winch.loss_of_power.random.getBoolValue() ); w.initNode("max-tow-length-m").setDoubleValue( config.winch.max_tow_length_m.getDoubleValue() ); w.initNode("max-spool-speed-m-s").setDoubleValue( config.winch.max_spool_speed.getDoubleValue() ); w.initNode("force-acceleration-N-s").setDoubleValue( config.winch.force_acceleration.getDoubleValue() ); w.initNode("spool-acceleration-m-s-s").setDoubleValue( config.winch.spool_acceleration.getDoubleValue() ); w.initNode("max-unspool-speed-m-s").setDoubleValue( config.winch.max_unspool_speed.getDoubleValue() ); w.initNode("max-force-N").setDoubleValue( config.winch.max_force.getDoubleValue() ); w.initNode("max-power-kW").setDoubleValue( config.winch.max_power.getDoubleValue() ); w.initNode("magic-constant").setDoubleValue( config.winch.magic_constant.getDoubleValue() ); w.initNode("messages/launch-signaller",1,"BOOL").setBoolValue( config.winch.messages.launch_signaller.getBoolValue() ); w.initNode("messages/winch-driver",1,"BOOL").setBoolValue( config.winch.messages.winch_driver.getBoolValue() ); w.initNode("messages/pilot",1,"BOOL").setBoolValue( config.winch.messages.pilot.getBoolValue() ); w.initNode("messages/remote-ac",1,"BOOL").setBoolValue( config.winch.messages.remote_ac.getBoolValue() ); io.write_properties( config_file, c ); } if( io.stat( config_file ) != nil ){ var config_properties = io.read_properties( config_file ); load_prop( config.aerotow.elastic_constant, config_properties.getNode("aerotow/elastic-constant") ); load_prop( config.aerotow.weight_per_m_kg_m, config_properties.getNode("aerotow/weight-per-m-kg-m") ); load_prop( config.aerotow.length, config_properties.getNode("aerotow/length") ); load_prop( config.aerotow.path_to_model, config_properties.getNode("aerotow/path_to_model") ); load_prop( config.aerotow.rope_diameter_mm, config_properties.getNode("aerotow/rope-diameter-mm") ); load_prop( config.winch.elastic_constant, config_properties.getNode("winch/elastic-constant") ); load_prop( config.winch.weight_per_m_kg_m, config_properties.getNode("winch/weight-per-m-kg-m") ); load_prop( config.winch.initial_tow_length, config_properties.getNode("winch/initial-tow-length-m") ); load_prop( config.winch.path_to_model, config_properties.getNode("winch/path_to_model") ); load_prop( config.winch.rope_diameter_mm, config_properties.getNode("winch/rope-diameter-mm") ); load_prop( config.winch.rope_breakage.height, config_properties.getNode("winch/breakage/height") ); load_prop( config.winch.rope_breakage.random, config_properties.getNode("winch/breakage/random") ); load_prop( config.winch.loss_of_power.height, config_properties.getNode("winch/loss-of-power/height") ); load_prop( config.winch.loss_of_power.random, config_properties.getNode("winch/loss-of-power/random") ); load_prop( config.winch.max_tow_length_m, config_properties.getNode("winch/max-tow-length-m") ); load_prop( config.winch.max_spool_speed, config_properties.getNode("winch/max-spool-speed-m-s") ); load_prop( config.winch.force_acceleration, config_properties.getNode("winch/force-acceleration-N-s") ); load_prop( config.winch.spool_acceleration, config_properties.getNode("winch/spool-acceleration-m-s-s") ); load_prop( config.winch.max_unspool_speed, config_properties.getNode("winch/max-unspool-speed-m-s") ); load_prop( config.winch.max_force, config_properties.getNode("winch/max-force-N") ); load_prop( config.winch.max_power, config_properties.getNode("winch/max-power-kW") ); load_prop( config.winch.magic_constant, config_properties.getNode("winch/magic-constant") ); load_prop( config.winch.messages.launch_signaller, config_properties.getNode("winch/messages/launch-signaller") ); load_prop( config.winch.messages.winch_driver, config_properties.getNode("winch/messages/winch-driver") ); load_prop( config.winch.messages.pilot, config_properties.getNode("winch/messages/pilot") ); load_prop( config.winch.messages.remote_ac, config_properties.getNode("winch/messages/remote-ac") ); } var aircraft_settings = { aerotow: { force_calc_by_other: check_or_create("sim/hitches/aerotow/force-is-calculated-by-other", 0, "BOOL"), # aircraft-specific is_slave: check_or_create("sim/hitches/aerotow/is-slave", 0, "BOOL"), # aircraft-specific break_force: check_or_create("sim/hitches/aerotow/tow/break-force", 12345.0, "DOUBLE"), # aircraft-specific local_pos: [ check_or_create("sim/hitches/aerotow/local-pos-x", 0.0, "DOUBLE"), # aircraft-specific check_or_create("sim/hitches/aerotow/local-pos-y", 0.0, "DOUBLE"), # aircraft-specific check_or_create("sim/hitches/aerotow/local-pos-z", 0.0, "DOUBLE"), # aircraft-specific ], }, winch: { typical_tow_speed: props.globals.getNode("sim/hitches/winch/typical-tow-speed-kph"), typical_tow_force: props.globals.getNode("sim/hitches/winch/typical-tow-force-N"), # Winch type: 0 = tow with constant force; 1 = tow with constant rope speed; aircraft-specific type_p: check_or_create("sim/hitches/winch/type", 0, "INT"), break_force: check_or_create("sim/hitches/winch/tow/break-force", 12345.0, "DOUBLE"), # aircraft-specific local_pos: [ check_or_create("sim/hitches/winch/local-pos-x", 0.0, "DOUBLE"), # aircraft-specific check_or_create("sim/hitches/winch/local-pos-y", 0.0, "DOUBLE"), # aircraft-specific check_or_create("sim/hitches/winch/local-pos-z", 0.0, "DOUBLE"), # aircraft-specific ], }, }; var check_aircraft_tow_settings = func{ if( aircraft_settings.winch.typical_tow_speed == nil ){ aircraft_settings.winch.typical_tow_speed = props.globals.initNode( "sim/hitches/winch/typical-tow-speed-kph" ); # Estimate typical tow speed from aircraft weight, this will inherently be a rough guess # Estimation basis: # type | typical tow speed | typical weight | reference # hang glider | 50 kph | 100 kg | https://www.safa.asn.au/resources/Tow%20Manual%20v5.4.1.pdf # Ka6 | 90 kph | 250 kg | http://www.smbc-eferding.at/wp-content/uploads/2014/01/Flughandbuch-Ka-6.pdf # LS8 | 120 kph | 500 kg | https://www.sglenzburg.ch/org/public/Dokumente/SGL/10-AFM/fhb_ls8a_rev3_tm8020.pdf # DG-1000S | 130 kph | 650 kg | http://adelaidesoaring.on.net/wp-content/uploads/2014/01/DG-1000s-Flight-Manual.pdf # # a quadratic regression for this data leads to y = -0.0002x2 + 0.3286x + 20.839, we use (simplified): f(x) = -0.0002 * x^2 + 0.33 * x + 21 var weight_kg = weight_lb.getDoubleValue() * LB2KG; aircraft_settings.winch.typical_tow_speed.setDoubleValue( -0.0002 * weight_kg * weight_kg + 0.33 * weight_kg + 21 ); } if( aircraft_settings.winch.typical_tow_force == nil ){ aircraft_settings.winch.typical_tow_force = props.globals.initNode( "sim/hitches/winch/typical-tow-force-N" ); aircraft_settings.winch.typical_tow_force.setDoubleValue( 0.15 * math.pow( weight_lb.getDoubleValue(), 1.25 ) * LB2KG * 9.81); } } # yasim properties for aerotow (should be already defined for yasim aircraft but not for JSBSim aircraft var aerotow_hash = { broken: props.globals.initNode("sim/hitches/aerotow/broken", 0, "BOOL"), force: props.globals.initNode("sim/hitches/aerotow/force", 0.0, "DOUBLE"), mp_auto_connect_period: props.globals.initNode("sim/hitches/aerotow/mp-auto-connect-period", 0.0, "DOUBLE"), mp_time_lag: props.globals.initNode("sim/hitches/aerotow/mp-time-lag", 0.0, "DOUBLE"), open: props.globals.initNode("sim/hitches/aerotow/open", 1, "BOOL"), #always init to true speed_tow_direction: props.globals.initNode("sim/hitches/aerotow/speed-in-tow-direction", 0.0, "DOUBLE"), old_open: props.globals.initNode("sim/hitches/aerotow/oldOpen", 1, "BOOL"), tow: { conn_ai_node: props.globals.initNode("sim/hitches/aerotow/tow/connected-to-ai-node", 0, "BOOL"), conn_ai_or_mp_callsign: props.globals.initNode("sim/hitches/aerotow/tow/connected-to-ai-or-mp-callsign", "", "STRING"), conn_ai_or_mp_id: props.globals.initNode("sim/hitches/aerotow/tow/connected-to-ai-or-mp-id", 0, "INT"), conn_mp_node: props.globals.initNode("sim/hitches/aerotow/tow/connected-to-mp-node", 0, "BOOL"), conn_prop_node: props.globals.initNode("sim/hitches/aerotow/tow/connected-to-property-node", 0, "BOOL"), dist: props.globals.initNode("sim/hitches/aerotow/tow/dist", 0.0, "DOUBLE"), end_force: [ props.globals.initNode("sim/hitches/aerotow/tow/end-force-x", 0.0, "DOUBLE"), props.globals.initNode("sim/hitches/aerotow/tow/end-force-y", 0.0, "DOUBLE"), props.globals.initNode("sim/hitches/aerotow/tow/end-force-z", 0.0, "DOUBLE"), ], node: props.globals.initNode("sim/hitches/aerotow/tow/node", "", "STRING"), length: props.globals.initNode("sim/hitches/aerotow/tow/length", 60.0, "DOUBLE"), }, rope: { exist: props.globals.initNode("sim/hitches/aerotow/rope/exist", 0, "BOOL"), model_id: props.globals.initNode("sim/hitches/aerotow/rope/model_id", -1, "INT"), lat: props.globals.initNode("ai/models/aerotowrope/position/latitude-deg", 0.0, "DOUBLE"), lon: props.globals.initNode("ai/models/aerotowrope/position/longitude-deg", 0.0, "DOUBLE"), alt: props.globals.initNode("ai/models/aerotowrope/position/altitude-ft", 0.0, "DOUBLE"), hdg: props.globals.initNode("ai/models/aerotowrope/orientation/true-heading-deg", 0.0, "DOUBLE"), pitch: props.globals.initNode("ai/models/aerotowrope/orientation/pitch-deg", 0.0, "DOUBLE"), }, }; # yasim properties for winch (should already be defined for yasim aircraft but not for JSBSim aircraft var winch_hash = { open: props.globals.initNode("sim/hitches/winch/open", 1, "BOOL"), #always init to true broken: props.globals.initNode("sim/hitches/winch/broken", 0, "BOOL"), type: 0, rope_breakage_p: props.globals.initNode("sim/hitches/winch/breakage/enabled", 0, "BOOL"), # NOT saved in config: Must be selected explicitly each time rope_breakage: 0, rope_breakage_height_int: 100.0, loss_of_power: { enabled_p: props.globals.initNode("sim/hitches/winch/loss-of-power/enabled", 0, "BOOL"), # NOT saved in config: Must be selected explicitly each time enabled: 0, height_int: 100.0, }, global_pos: [ props.globals.initNode("sim/hitches/winch/winch/global-pos-x", 0.0, "DOUBLE"), props.globals.initNode("sim/hitches/winch/winch/global-pos-y", 0.0, "DOUBLE"), props.globals.initNode("sim/hitches/winch/winch/global-pos-z", 0.0, "DOUBLE"), ], tow: { length: props.globals.initNode("sim/hitches/winch/tow/length", 0.0, "DOUBLE"), dist: props.globals.initNode("sim/hitches/winch/tow/dist", 0.0, "DOUBLE"), }, old_open: props.globals.initNode("sim/hitches/winch/oldOpen", 1, "BOOL"), rope: { exist: props.globals.initNode("sim/hitches/winch/rope/exist", 0, "BOOL"), model_id: props.globals.initNode("sim/hitches/winch/rope/model_id", -1, "INT"), lat: props.globals.initNode("ai/models/winchrope/position/latitude-deg", 0.0, "DOUBLE"), lon: props.globals.initNode("ai/models/winchrope/position/longitude-deg", 0.0, "DOUBLE"), alt: props.globals.initNode("ai/models/winchrope/position/altitude-ft", 0.0, "DOUBLE"), hdg: props.globals.initNode("ai/models/winchrope/orientation/true-heading-deg", 0.0, "DOUBLE"), pitch: props.globals.initNode("ai/models/winchrope/orientation/pitch-deg", 0.0, "DOUBLE"), }, }; var force_setting = 0.0; # used with constant force type winch var speed_setting = 0.0; # used with constant speed type winch if ( fdm == "jsb" ) { # new properties for JSBSim aerotow aircraft_settings.aerotow.force_name_jsbsim = check_or_create("sim/hitches/aerotow/force_name_jsbsim", "hitch", "STRING"); # aircraft-specific aircraft_settings.aerotow.decoupled_locations = check_or_create("sim/hitches/aerotow/decoupled-force-and-rope-locations", 0, "BOOL"); # aircraft-specific aircraft_settings.winch.force_name_jsbsim = check_or_create("sim/hitches/winch/force_name_jsbsim", "hitch", "STRING"); # aircraft-specific aircraft_settings.winch.automatic_release_angle = check_or_create("sim/hitches/winch/automatic-release-angle-deg", 361.0, "DOUBLE"); # aircraft-specific aircraft_settings.winch.decoupled_locations = check_or_create("sim/hitches/winch/decoupled-force-and-rope-locations", 0, "BOOL"); # aircraft-specific aerotow_hash.mp_old_open = props.globals.initNode("sim/hitches/aerotow/mp_oldOpen", 1, "BOOL"); aerotow_hash.tow.mp_last_reported_dist = props.globals.initNode("sim/hitches/aerotow/tow/mp_last_reported_dist", 0.0, "DOUBLE"); # new properties for JSBSim winch winch_hash.clutched = props.globals.initNode("sim/hitches/winch/winch/clutched", 0, "BOOL"); winch_hash.actual_spool_speed = props.globals.initNode("sim/hitches/winch/winch/actual-spool-speed-m-s", 0.0, "DOUBLE"); winch_hash.actual_force = props.globals.initNode("sim/hitches/winch/winch/actual-force-N", 0.0, "DOUBLE"); var hitchname = aircraft_settings.winch.force_name_jsbsim.getValue(); winch_hash.apply_force = { magnitude: props.globals.initNode("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/magnitude", 0.0, "DOUBLE"), coord: [ check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/x", 0.0, "DOUBLE"), # aircraft-specific check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/y", 0.0, "DOUBLE"), # aircraft-specific check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/z", 0.0, "DOUBLE"), # aircraft-specific ], }; var hitchname = aircraft_settings.aerotow.force_name_jsbsim.getValue(); aerotow_hash.apply_force = { magnitude: props.globals.initNode("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/magnitude", 0.0, "DOUBLE"), coord: [ check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/x", 0.0, "DOUBLE"), # aircraft-specific check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/y", 0.0, "DOUBLE"), # aircraft-specific check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/z", 0.0, "DOUBLE"), # aircraft-specific ], }; # consider older JSBSim-versions which do NOT provide the locations of external_reactions in the property tree if( getprop("fdm/jsbsim/external_reactions/" ~ aircraft_settings.aerotow.force_name_jsbsim.getValue() ~ "/location-x-in") == nil ) { aircraft_settings.aerotow.decoupled_locations.setBoolValue( 1 ); } if( getprop("fdm/jsbsim/external_reactions/" ~ aircraft_settings.winch.force_name_jsbsim.getValue() ~ "/location-x-in") == nil ) { aircraft_settings.winch.decoupled_locations.setBoolValue( 1 ); } setlistener(aircraft_settings.winch.force_name_jsbsim , func() { var hitchname = aircraft_settings.winch.force_name_jsbsim.getValue(); set_force_apply( winch_hash, hitchname ); }); setlistener(aircraft_settings.aerotow.force_name_jsbsim , func() { var hitchname = aircraft_settings.aerotow.force_name_jsbsim.getValue(); set_force_apply( aerotow_hash, hitchname ); }); } var callsign = check_or_create("sim/multiplay/callsign", "", "STRING"); # session-specific var message_pilot = props.globals.getNode("sim/messages/pilot", 1); var message_ai = props.globals.getNode("sim/messages/ai-plane", 1); var message_atc = props.globals.getNode("sim/messages/atc", 1); var write_message = func( type, message ){ if( type == "launch-signaller" ){ if( config.winch.messages.launch_signaller.getBoolValue() ) message_atc.setValue( message ); } elsif( type == "winch-driver" ){ if( config.winch.messages.winch_driver.getBoolValue() ) message_ai.setValue( message ); } elsif( type == "pilot" ){ if( config.winch.messages.pilot.getBoolValue() ) message_pilot.setValue( message ); } elsif( type == "remote-ac" ){ if( config.winch.messages.remote_ac.getBoolValue() ) message_ai.setValue( message ); } elsif( type == "system" ){ message_atc.setValue( message ); } else { screen.log.write( message ~ " [type: unknown]"); } } var delta_time = props.globals.getNode("sim/time/delta-sec", 1); var orientation = [ props.globals.getNode("orientation/heading-deg", 1), props.globals.getNode("orientation/roll-deg", 1), props.globals.getNode("orientation/pitch-deg", 1), ]; var wind_from = check_or_create("environment/wind-from-heading-deg", 0.0, "DOUBLE"); var wind_speed_kt = check_or_create("environment/wind-speed-kt", 0.0, "DOUBLE"); var set_force_apply = func ( hash, hitchname ){ hash.apply_force = { magnitude: check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/magnitude", 0.0, "DOUBLE"), coord: [ check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/x", 0.0, "DOUBLE"), check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/y", 0.0, "DOUBLE"), check_or_create("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/z", 0.0, "DOUBLE"), ], }; } var loss_of_power = func () { # The maximum power is lost to 1/10 of it over a time span of 3 +- 1 seconds interpolate( config.winch.max_power, config.winch.max_power.getDoubleValue() / 10, 2 + 2 * rand() ); # Afterwards it is completely lost after another 2 +- 1 seconds settimer( func() { interpolate( config.winch.max_power, 0, 1 + 2 * rand() ); }, 3); } # ###################################################################################################################### # main function # ###################################################################################################################### var towing = func { var dt = 0; # ------------------------------- aerotow part ------------------------------- var open = aerotow_hash.open.getBoolValue(); var oldOpen = aerotow_hash.old_open.getBoolValue(); if ( open != oldOpen ) { # check if my hitch state has changed, if yes: message if ( !open ) { # my hitch was open and is closed now if ( fdm == "jsb" ) { var distance = aerotow_hash.tow.dist.getDoubleValue(); var towlength_m = aerotow_hash.tow.length.getDoubleValue(); if ( distance > towlength_m * 1.0001 ) { write_message( "system", sprintf("Could not lock hitch (tow length is insufficient) on hitch %i!", aerotow_hash.tow.conn_mp_node.getIntValue()) ); aerotow_hash.open.setBoolValue( 1 ); # open my hitch again } else { # mp aircraft to far away # my hitch is closed write_message( "system", sprintf("Locked hitch aerotow %i!", aerotow_hash.tow.conn_mp_node.getIntValue()) ); } aerotow_hash.broken.setBoolValue(0); } # end: JSBSim if ( !aerotow_hash.open.getBoolValue() ) { # setup ai-towrope createTowrope("aerotow"); # set default hitch coordinates (needed for Ai- and non-interactive MP aircraft) setAIObjectDefaults() ; } } # end hitch is closed if ( open ) { # my hitch is now open if ( fdm == "jsb" ) { if ( aerotow_hash.broken.getBoolValue() ) { #getprop("sim/hitches/aerotow/broken") write_message( "pilot", "Oh no, the tow is broken" ); } else { write_message( "pilot", sprintf("Opened hitch aerotow %i!", aerotow_hash.tow.conn_mp_node.getIntValue() ) ); } releaseHitch("aerotow"); # open=1 / forces=0 } # end: JSBSim removeTowrope("aerotow"); # remove towrope model } # end hitch is open aerotow_hash.old_open.setBoolValue( open ); } # end hitch state has changed if (!open ) { aerotow(open); # end hitch is closed (open == 0) } else { # my hitch is open var mp_auto_connect_period = aerotow_hash.mp_auto_connect_period.getDoubleValue(); if ( mp_auto_connect_period != 0 ) { # if auto-connect if ( fdm == "jsb" ) { # only for JSBSim aircraft findBestAIObject(); } # end JSBSim aircraft dt = mp_auto_connect_period; } else { # my hitch is open and not auto-connect dt = 0; } } # winch part if (!winch_hash.open.getBoolValue() ) { winch(winch_hash.open.getBoolValue()); } settimer( towing, dt ); } # end towing setlistener( winch_hash.open, func { var winchopen = winch_hash.open.getBoolValue(); var wincholdOpen = winch_hash.old_open.getBoolValue(); if ( winchopen != wincholdOpen ) { # check if my hitch state has changed, if yes: message if ( !winchopen ) { # my hitch was open and is closed now if ( fdm == "jsb" ) { var distance = winch_hash.tow.dist.getDoubleValue(); var towlength_m = winch_hash.tow.length.getDoubleValue(); if ( distance > towlength_m ) { write_message( "system", sprintf("Could not lock hitch (tow length is insufficient) on hitch %i!", aerotow_hash.tow.conn_mp_node.getIntValue() ) ); winch_hash.open.setBoolValue( 1 ); # open my hitch again } else { # mp aircraft to far away # my hitch is closed write_message( "system", sprintf("Locked hitch winch %i!", aerotow_hash.tow.conn_mp_node.getIntValue() ) ); winch_hash.clutched.setBoolValue( 0 );; } winch_hash.broken.setBoolValue( 0 ); winch_hash.actual_spool_speed.setDoubleValue( 0.0 ); } # end: JSBSim if ( !winch_hash.open.getBoolValue() ) { # setup ai-towrope createTowrope("winch"); # set default hitch coordinates (needed for Ai- and non-interactive MP aircraft) setAIObjectDefaults() ; } } # end hitch is closed if ( winchopen ) { # my hitch is now open if ( fdm == "jsb" ) { if ( winch_hash.broken.getBoolValue() ) { write_message( "pilot", "Oh no, the tow is broken" ); } releaseHitch("winch"); } # end: JSBSim pull_in_rope(); } # end hitch is open winch_hash.old_open.setBoolValue( winchopen ); } # end hitch state has changed }); setlistener( aircraft_settings.winch.type_p, func { winch_hash.type = aircraft_settings.winch.type_p.getIntValue(); }); # ###################################################################################################################### # find best AI object # ###################################################################################################################### var findBestAIObject = func (){ # the nearest found plane, that is close enough will be used # set some default variables, needed later to identify if the found object is # an AI-Object, a "non-interactiv MP-Object or an interactive MP-Object # local variables var aiobjects = []; # keeps the ai-planes from the property tree var aiPosition = geo.Coord.new(); # current processed ai-plane var myPosition = geo.Coord.new(); # coordinates of glider var distance_m = 0; # distance to ai-plane var nodeIsAiAircraft = 0; var nodeIsMpAircraft = 0; var running_as_autoconnect = 0; var mp_open_last_state = 0; var isSlave = 0; if ( fdm == "yasim" ) return; # bypass this routine for Yasim-aircraft if (aerotow_hash.mp_auto_connect_period.getDoubleValue() != 0 ) { var running_as_autoconnect = 1; } var towlength_m = aerotow_hash.tow.length.getDoubleValue(); var bestdist_m = towlength_m; # initial value myPosition = geo.aircraft_position(); # todo: calculate exact hitch position if( running_as_autoconnect ) { var mycallsign = callsign.getValue(); } var found = 0; aiobjects = props.globals.getNode("ai/models").getChildren(); foreach (var aimember; aiobjects) { if ( (var node = aimember.getName() ) != nil ) { nodeIsAiAircraft = 0; nodeIsMpAircraft = 0; if ( sprintf("%8s",node) == "aircraft" or sprintf("%7s",node) == "dragger" ) nodeIsAiAircraft = 1; if ( sprintf("%11s",node) == "multiplayer" ) nodeIsMpAircraft = 1; if ( aimember.getNode("valid") == nil ) continue; if ( !nodeIsAiAircraft and !nodeIsMpAircraft) continue; if ( !aimember.getNode("valid").getValue() ) continue; # node is invalid if( running_as_autoconnect ) { if ( !nodeIsMpAircraft ) continue; #if ( aimember.getValue("sim/hitches/aerotow/open") == nil ) continue; # this node MUST exist for mp-aircraft which want to be towed #if ( aimember.getValue("sim/hitches/aerotow/open") == 1 ) continue; # if mp hook open, auto-connect is NOT possible if ( aimember.getValue("sim/hitches/aerotow/open") != 0 ) continue; if (mycallsign != aimember.getValue("sim/hitches/aerotow/tow/connected-to-ai-or-mp-callsign") ) continue ; # I am the wrong one if ( !aerotow_hash.mp_old_open.getBoolValue() ) continue; # this prevents an unwanted immediate auto-connect after the dragger # released its hitch. Firstly wait for a reported "open" hitch from glider } var lat_deg = aimember.getNode("position/latitude-deg").getValue(); var lon_deg = aimember.getNode("position/longitude-deg").getValue(); var alt_m = aimember.getNode("position/altitude-ft").getValue() * FT2M; var aiPosition = geo.Coord.set_latlon( lat_deg, lon_deg, alt_m ); distance_m = (myPosition.distance_to(aiPosition)); if ( distance_m < bestdist_m ) { bestdist_m = distance_m; var towEndNode = node; var nodeID = aimember.getNode("id").getValue(); var aicallsign = aimember.getNode("callsign").getValue(); #set properties aerotow_hash.open.setBoolValue( 0 ); aerotow_hash.tow.conn_ai_node.setBoolValue( nodeIsAiAircraft ); aerotow_hash.tow.conn_mp_node.setBoolValue( nodeIsMpAircraft ); aerotow_hash.tow.conn_ai_or_mp_callsign.setValue( aicallsign ); aerotow_hash.tow.conn_ai_or_mp_id.setIntValue( nodeID ); aerotow_hash.tow.conn_prop_node.setBoolValue( 1 ); aerotow_hash.tow.node.setValue( towEndNode ); aerotow_hash.tow.dist.setDoubleValue( bestdist_m ); aerotow_hash.tow.mp_last_reported_dist.setDoubleValue( 0.0 ); # Set some dummy values. In case of an "interactive"-MP plane # the correct values will be transmitted in the following loop aimember.getNode("sim/hitches/aerotow/local-pos-x",1).setValue(-5.); aimember.getNode("sim/hitches/aerotow/local-pos-y",1).setValue(0.); aimember.getNode("sim/hitches/aerotow/local-pos-z",1).setValue(0.); aimember.getNode("sim/hitches/aerotow/tow/dist",1).setValue(-1.); found = 1; } # end distance_m < bestdist_m } # end node != nil } # end loop aiobjects if (found) { if ( !running_as_autoconnect) { write_message( "pilot", sprintf("%s, I am on your hook, distance %4.3f meter.",aicallsign,bestdist_m) ); } else { write_message( "remote-ac", sprintf("%s: I am on your hook, distance %4.3f meter.",aicallsign,bestdist_m ) ); } if ( running_as_autoconnect ) { isSlave = 1; aerotow_hash.is_slave.setBoolValue( isSlave ); } aerotow_hash.mp_old_open.setBoolValue( 1 ); } # end: if found else { if (!running_as_autoconnect) { write_message( "system", "Sorry, no aircraft for aerotow!" ); } else{ aerotow_hash.old_open.setBoolValue( 1 ); } } } # End function findBestAIObject # ###################################################################################################################### # Start the towing animation ASAP towing(); # ###################################################################################################################### # aerotow function # ###################################################################################################################### var aerotow = func (open){ #print("function aerotow is running"); # if (!open ) { ########################################### my hitch position ############################################ myPosition = geo.aircraft_position(); var my_head_deg = orientation[0].getDoubleValue(); var my_roll_deg = orientation[1].getDoubleValue(); var my_pitch_deg = orientation[2].getDoubleValue(); # hook coordinates in Yasim-system (x-> nose / y -> left wing / z -> up) assignHitchLocations("aerotow"); var x = aircraft_settings.aerotow.local_pos[0].getDoubleValue(); var y = aircraft_settings.aerotow.local_pos[1].getDoubleValue(); var z = aircraft_settings.aerotow.local_pos[2].getDoubleValue(); var alpha_deg = my_roll_deg * (1.); # roll clockwise (looking in x-direction) := + var beta_deg = my_pitch_deg * (-1.); # pitch clockwise (looking in y-direction) := - # transform hook coordinates var Xn = PointRotate3D(x:x,y:y,z:z,xr:0.,yr:0.,zr:0.,alpha_deg:alpha_deg,beta_deg:beta_deg,gamma_deg:0.); var install_distance_m = Xn[0]; # in front of ref-point of glider var install_side_m = Xn[1]; var install_alt_m = Xn[2]; var myHitch_pos = myPosition.apply_course_distance( my_head_deg , install_distance_m ); var myHitch_pos = myPosition.apply_course_distance( my_head_deg - 90. , install_side_m ); myHitch_pos.set_alt(myPosition.alt() + install_alt_m); ########################################### ai hitch position ############################################ #var aiNodeID = getprop("sim/hitches/aerotow/tow/connected-to-ai-or-mp-id"); # id of former found ai/mp aircraft #print("aiNodeID=",aiNodeID); var aiCallsign = aerotow_hash.tow.conn_ai_or_mp_callsign.getValue(); # callsign of former found ai/mp aircraft var found = 0; aiobjects = props.globals.getNode("ai/models").getChildren(); foreach (var aimember; aiobjects) { if ( (var c = aimember.getNode("id") ) != nil ) { if ( !aimember.getNode("valid").getValue() ) continue; # node is invalid # Identifying the MP-aircraft by its node-id works fine with JSBSim-aircraft but NOT with YASim. # In YASim the node-id is not updated which could lead to complications (e.g. node-id changes after "Pause" or "Exit"). #var testprop = c.getValue(); #if ( testprop == aiNodeID) { # Identifying the MP-aircraft by its callsign works fine with JSBSim AND YASim-aircraft var testprop = aimember.getNode("callsign").getValue(); if ( testprop == aiCallsign ) { found = found + 1; ###################### check status of ai hitch ###################### if ( fdm == "jsb" ) { # check if the multiplayer hitch state has changed # this trick avoids immediate opening after locking because MP-aircraft has not yet reported a locked hitch if ( (var d = aimember.getNode("sim/hitches/aerotow/open") ) != nil ) { var mpOpen = aimember.getNode("sim/hitches/aerotow/open").getValue(); var mp_oldOpen = aerotow_hash.mp_old_open.getBoolValue(); #print('mpOpen=',mpOpen,' mp_oldOpen=',mp_oldOpen); if ( mpOpen != mp_oldOpen ) { # state has changed: was open and is now locked OR was locked and is now open if ( mpOpen ) { write_message( "remote-ac", sprintf("%s: I have released the tow!", aerotow_hash.tow.conn_ai_or_mp_callsign.getValue() ) ); releaseHitch("aerotow"); # my open=1 / forces=0 / remove towrope } # end: open aerotow_hash.mp_old_open.setBoolValue(mpOpen); } # end: state has changed } # end: node is available } #end : JSBSim ######################################################################## # get coordinates var ai_lat = aimember.getNode("position/latitude-deg").getValue(); var ai_lon = aimember.getNode("position/longitude-deg").getValue(); var ai_alt = (aimember.getNode("position/altitude-ft").getValue()) * FT2M; #print("ai_lat,lon,alt",ai_lat,ai_lon,ai_alt); var ai_pitch_deg = aimember.getNode("orientation/pitch-deg").getValue(); var ai_roll_deg = aimember.getNode("orientation/roll-deg").getValue(); var ai_head_deg = aimember.getNode("orientation/true-heading-deg").getValue(); var aiHitchX = aimember.getNode("sim/hitches/aerotow/local-pos-x").getValue(); var aiHitchY = aimember.getNode("sim/hitches/aerotow/local-pos-y").getValue(); var aiHitchZ = aimember.getNode("sim/hitches/aerotow/local-pos-z").getValue(); var aiPosition = geo.Coord.set_latlon( ai_lat, ai_lon, ai_alt ); var alpha_deg = ai_roll_deg * (1.); var beta_deg = ai_pitch_deg * (-1.); # transform hook coordinates var Xn = PointRotate3D(x:aiHitchX,y:aiHitchY,z:aiHitchZ,xr:0.,yr:0.,zr:0.,alpha_deg:alpha_deg,beta_deg:beta_deg,gamma_deg:0.); var install_distance_m = Xn[0]; # in front of ref-point of glider var install_side_m = Xn[1]; var install_alt_m = Xn[2]; var aiHitch_pos = aiPosition.apply_course_distance( ai_head_deg , install_distance_m ); var aiHitch_pos = aiPosition.apply_course_distance( ai_head_deg - 90. , install_side_m ); aiHitch_pos.set_alt(aiPosition.alt() + install_alt_m); ########################################### distance between hitches ##################################### var distance = (myHitch_pos.direct_distance_to(aiHitch_pos)); # distance to plane in meter var aiHitchheadto = (myHitch_pos.course_to(aiHitch_pos)); var height = myHitch_pos.alt() - aiHitch_pos.alt(); var aiHitchpitchto = -math.asin((myHitch_pos.alt()-aiHitch_pos.alt())/distance) / 0.01745; #print(" pitch: ", aiHitchpitchto); # update position of rope aerotow_hash.rope.lat.setDoubleValue( myHitch_pos.lat() ); aerotow_hash.rope.lon.setDoubleValue( myHitch_pos.lon() ); aerotow_hash.rope.alt.setDoubleValue( myHitch_pos.alt() * M2FT ); # update orientation of rope aerotow_hash.rope.hdg.setDoubleValue( aiHitchheadto ); aerotow_hash.rope.pitch.setDoubleValue( aiHitchpitchto ); # update length of rope aerotow_hash.tow.dist.setDoubleValue( distance ); ############################################# calc forces ################################################## # calc forces only for JSBSim-aircraft # tow-end-forces must be reported in N to be consiststent to Yasim-aircraft # hitch-forces must be LBS to be consistent to the JSBSim "external_forces/.../magnitude" definition if ( fdm == "jsb" ) { # check if the MP-aircraft properties have been updated. If not (maybe due to time-lag) bypass force calculation (use previous forces instead) var mp_reported_dist = aimember.getNode("sim/hitches/aerotow/tow/dist").getValue(); var mp_last_reported_dist = aerotow_hash.tow.mp_last_reported_dist.getDoubleValue(); var mp_delta_reported_dist = mp_reported_dist - mp_last_reported_dist ; aerotow_hash.tow.mp_last_reported_dist.setDoubleValue( mp_reported_dist ); var mp_delta_reported_dist2 = mp_delta_reported_dist * mp_delta_reported_dist ; # we need the absolute value if ( (mp_delta_reported_dist2 > 0.0000001) or (mp_reported_dist < 0. )){ # we have the updated MP coordinates (no time lag) # or the MP-aircraft is a non-interactive mp plane (mp_reported_dist = -1) # => update forces else use the old forces! var breakforce_N = getprop("sim/hitches/aerotow/tow/break-force"); # could be different in both aircraft var isSlave = aerotow_hash.is_slave.getBoolValue(); if ( !isSlave ){ # if we are master, we have to calculate the forces var elastic_constant = getprop("sim/hitches/aerotow/tow/elastic-constant"); var towlength_m = getprop("sim/hitches/aerotow/tow/length"); var delta_towlength_m = distance - towlength_m; if ( delta_towlength_m < 0. ) { var forcetow_N = 0.; } else{ var forcetow_N = elastic_constant * delta_towlength_m / towlength_m; } } else { # we are slave and get the forces from master var forcetowX_N = aimember.getNode("sim/hitches/aerotow/tow/end-force-x").getValue() * 1; var forcetowY_N = aimember.getNode("sim/hitches/aerotow/tow/end-force-y").getValue() * 1; var forcetowZ_N = aimember.getNode("sim/hitches/aerotow/tow/end-force-z").getValue() * 1; var forcetow_N = math.sqrt( forcetowX_N * forcetowX_N + forcetowY_N * forcetowY_N + forcetowZ_N * forcetowZ_N ); } var forcetow_LBS = forcetow_N * 0.224809; # N -> LBF if ( forcetow_N < breakforce_N ) { var distancepr = (myHitch_pos.distance_to(aiHitch_pos)); # correct a failure, if the projected length is larger than direct length if (distancepr > distance) { distancepr = distance;} var alpha = math.acos( (distancepr / distance) ); if ( aiHitch_pos.alt() > myHitch_pos.alt()) alpha = - alpha; var beta = ( aiHitchheadto - my_head_deg ) * 0.01745; var gamma = my_pitch_deg * 0.01745; var delta = my_roll_deg * 0.01745; var sina = math.sin(alpha); var cosa = math.cos(alpha); var sinb = math.sin(beta); var cosb = math.cos(beta); var sing = math.sin(gamma); var cosg = math.cos(gamma); var sind = math.sin(delta); var cosd = math.cos(delta); var forcetow = forcetow_LBS; # we deliver LBS to JSBSim # calculate unit vector of force direction in JSBSim-system var force = 1; # global forces: alpha beta var fglobalx = force * cosa * cosb; var fglobaly = force * cosa * sinb; var fglobalz = force * sina; # local forces by pitch: gamma var flpitchx = fglobalx * cosg - fglobalz * sing; var flpitchy = fglobaly; var flpitchz = fglobalx * sing + fglobalz * cosg; # local forces by roll: delta var flrollx = flpitchx; var flrolly = flpitchy * cosd + flpitchz * sind; var flrollz = - flpitchy * sind + flpitchz * cosd; # asigning to LOCAL coord of plane var forcex = flrollx; var forcey = flrolly; var forcez = flrollz; # JSBSim-body-frame: x-> nose / y -> right wing / z -> down # apply forces to hook (forces are in LBS or N see above) var hitchname = getprop("sim/hitches/aerotow/force_name_jsbsim"); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/magnitude", forcetow); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/x", forcex); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/y", forcey); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/z", forcez); } else { # rope is broken aerotow_hash.broken.setBoolValue( 1 ); #setprop("sim/messages/atc", sprintf("Oh no, the tow is broken")); releaseHitch("aerotow"); # open=1 / forces=0 / remove towrope } ############################################# report forces ############################################## # if we are connected to a MP-aircraft and master var nodeIsMpAircraft = getprop("sim/hitches/aerotow/tow/connected-to-mp-node"); if ( nodeIsMpAircraft and !isSlave ){ # transform my hitch coordinates to cartesian earth coordinates var myHitchCartEarth = geodtocart(myHitch_pos.lat(),myHitch_pos.lon(),myHitch_pos.alt() ); var myHitchXearth_m = myHitchCartEarth[0]; var myHitchYearth_m = myHitchCartEarth[1]; var myHitchZearth_m = myHitchCartEarth[2]; # transform MP hitch coordinates to cartesian earth coordinates var aiHitchCartEarth = geodtocart(aiHitch_pos.lat(),aiHitch_pos.lon(),aiHitch_pos.alt() ); var aiHitchXearth_m = aiHitchCartEarth[0]; var aiHitchYearth_m = aiHitchCartEarth[1]; var aiHitchZearth_m = aiHitchCartEarth[2]; # calculate normal vector in tow direction in cartesian earth coordinates var dx = aiHitchXearth_m - myHitchXearth_m; var dy = aiHitchYearth_m - myHitchYearth_m; var dz = aiHitchZearth_m - myHitchZearth_m; var dl = math.sqrt( dx * dx + dy * dy + dz * dz ); var forcetowX_N = forcetow_N * dx / dl; var forcetowY_N = forcetow_N * dy / dl; var forcetowZ_N = forcetow_N * dz / dl; setprop("sim/hitches/aerotow/tow/dist", distance); setprop("sim/hitches/aerotow/tow/end-force-x", -forcetowX_N); # force acts in setprop("sim/hitches/aerotow/tow/end-force-y", -forcetowY_N); # opposite direction setprop("sim/hitches/aerotow/tow/end-force-z", -forcetowZ_N); # at tow end } # end report forces } # end: timelag } # end forces/JSBSim } # end: aiNodeID } # end: check id != nil } # end: loop over aiobjects if ( found == 0 ) { if ( fdm == "jsb" ) { write_message( "system", "MP-aircraft disappeared!" ); aerotow_hash.open.setBoolValue( 1 ); # open my hitch aerotow_hash.tow.conn_ai_or_mp_id.setIntValue( 0 ); aerotow_hash.tow.conn_ai_or_mp_callsign.setValue( "" ); aerotow_hash.tow.conn_ai_node.setBoolValue( 0 ); aerotow_hash.tow.conn_mp_node.setBoolValue( 0 ); aerotow_hash.tow.conn_prop_node.setBoolValue( 0 ); } #if ( fdm == "yasim" ) removeTowrope("aerotow"); # remove towrope model } # end found=0 } # end function aerotow # ###################################################################################################################### # winch function # ###################################################################################################################### var winch = func (open){ if (!open ) { ########################################### my hitch position ############################################ myPosition = geo.aircraft_position(); var my_head_deg = orientation[0].getDoubleValue(); var my_roll_deg = orientation[1].getDoubleValue(); var my_pitch_deg = orientation[2].getDoubleValue(); # hook coordinates in Yasim-system (x-> nose / y -> left wing / z -> up) assignHitchLocations("winch"); var x = aircraft_settings.winch.local_pos[0].getDoubleValue(); var y = aircraft_settings.winch.local_pos[1].getDoubleValue(); var z = aircraft_settings.winch.local_pos[2].getDoubleValue(); var alpha_deg = my_roll_deg * (1.); # roll clockwise (looking in x-direction) := + var beta_deg = my_pitch_deg * (-1.); # pitch clockwise (looking in y-direction) := - # transform hook coordinates var Xn = PointRotate3D(x:x,y:y,z:z,xr:0.,yr:0.,zr:0.,alpha_deg:alpha_deg,beta_deg:beta_deg,gamma_deg:0.); var install_distance_m = Xn[0]; # in front of ref-point of glider var install_side_m = Xn[1]; var install_alt_m = Xn[2]; var myHitch_pos = myPosition.apply_course_distance( my_head_deg , install_distance_m ); var myHitch_pos = myPosition.apply_course_distance( my_head_deg - 90. , install_side_m ); myHitch_pos.set_alt(myPosition.alt() + install_alt_m); ############################ Check for rope breakage ############################### var my_height = myHitch_pos.alt() - geo.elevation( myHitch_pos.lat(), myHitch_pos.lon() ); if( winch_hash.rope_breakage and my_height > winch_hash.rope_breakage_height_int ) { releaseHitch("winch"); winch_hash.rope_breakage = 0; # The pilot shouldn't immediately be notified this was a rope breakage (to train recognizing a rope breakage) # That's why we report the breakage and its height 5 seconds later var temp = winch_hash.rope_breakage_height_int; settimer( func() { write_message( "winch-driver", "Rope broke at " ~ sprintf("%4d", math.round( temp ) ) ~ "m height" ); }, 5); return; } ############################ Check for winch loss of power ########################## if( winch_hash.loss_of_power.enabled and my_height > winch_hash.loss_of_power.height_int ) { loss_of_power(); winch_hash.loss_of_power.enabled = 0; # The pilot shouldn't immediately be notified this was a loss of power (to train recognizing a winch loss of power) # That's why we report the loss of power and its height 15 seconds later var temp = winch_hash.loss_of_power.height_int; settimer( func() { write_message( "winch-driver", "Winch lost power at " ~ sprintf("%4d", math.round( temp ) ) ~ "m height" ); }, 15); return; } ########################################### winch hitch position ############################################ # get coordinates var winch_global_pos_x = winch_hash.global_pos[0].getDoubleValue(); var winch_global_pos_y = winch_hash.global_pos[1].getDoubleValue(); var winch_global_pos_z = winch_hash.global_pos[2].getDoubleValue(); var winch_geod = carttogeod(winch_global_pos_x, winch_global_pos_y, winch_global_pos_z); var ai_lat = winch_geod[0]; var ai_lon = winch_geod[1]; #var ai_alt = winch_geod[2] * FT2M; var ai_alt = winch_geod[2]; #print("ai_lat,lon,alt",ai_lat,ai_lon,ai_alt); var aiHitch_pos = geo.Coord.set_latlon( ai_lat, ai_lon, ai_alt ); ########################################### distance between hitches ##################################### var distance = (myHitch_pos.direct_distance_to(aiHitch_pos)); # distance to winch in meter var aiHitchheadto = (myHitch_pos.course_to(aiHitch_pos)); var height = myHitch_pos.alt() - aiHitch_pos.alt(); var aiHitchpitchto = -math.asin((myHitch_pos.alt()-aiHitch_pos.alt())/distance) * R2D; #print(" pitch: ", aiHitchpitchto); #print("Update Winch Rope"); #print("Winch Rope Lat is "~ myHitch_pos.lat() ); # update position of rope winch_hash.rope.lat.setDoubleValue( myHitch_pos.lat() ); winch_hash.rope.lon.setDoubleValue( myHitch_pos.lon() ); winch_hash.rope.alt.setDoubleValue( myHitch_pos.alt() * M2FT ); # update orientation of rope winch_hash.rope.hdg.setDoubleValue( aiHitchheadto ); winch_hash.rope.pitch.setDoubleValue( aiHitchpitchto ); # update length of rope winch_hash.tow.dist.setDoubleValue( distance ); #print("distance=",distance); ############################################# calc forces ################################################## # calc forces only for JSBSim-aircraft # tow-end-forces must be reported in N to be consiststent to Yasim-aircraft # hitch-forces must be LBS to be consistent to the JSBSim "external_forces/.../magnitude" definition if ( fdm == "jsb" ) { var spool_max = config.winch.max_spool_speed.getDoubleValue(); var unspool_max = config.winch.max_unspool_speed.getDoubleValue(); var max_force_N = config.winch.max_force.getDoubleValue(); var max_power_W = config.winch.max_power.getDoubleValue() * 1000.; var breakforce_N = aircraft_settings.winch.break_force.getDoubleValue(); var elastic_constant = config.winch.elastic_constant.getDoubleValue(); var towlength_m = winch_hash.tow.length.getDoubleValue(); var max_tow_length_m = config.winch.max_tow_length_m.getDoubleValue(); var spoolspeed = winch_hash.actual_spool_speed.getDoubleValue(); var spool_acceleration = config.winch.spool_acceleration.getDoubleValue(); var delta_t = delta_time.getDoubleValue(); #print("towlength_m= ", towlength_m , " elastic_constant= ", elastic_constant," delta_towlength_m= ", delta_towlength_m); var forcetow_N = winch_hash.actual_force.getDoubleValue(); var towlength_new_m = nil; var delta_towlength_m = nil; if ( winch_hash.clutched.getBoolValue() ) { if( winch_hash.type == 0 ){ # Concept: force is given forcetow_N = force_setting; } else if ( winch_hash.type == 1 ){ # Concept: regulate force to reach target speed var speed_trgt = speed_setting; if( math.abs ( spoolspeed - speed_trgt ) > 0.5 ) { if( speed_trgt > spoolspeed ) { forcetow_N += acceleration_N_s * delta_t; } else if ( speed_trgt < spoolspeed ){ forcetow_N -= acceleration_N_s * delta_t; } } } # New towlength is distance from hook to winch; previous towlength is stored in winch_hash.towlength_m towlength_new_m = distance; delta_towlength_m = -1 * ( distance - towlength_m ); spoolspeed = delta_towlength_m / delta_t; } else { # un-clutched # --- experimental --- # towlength_new_m = towlength_m - spoolspeed * delta_t; delta_towlength_m = distance - towlength_new_m; # we assume that the the winch-operator avoids tow sagging ( => rigid rope; negativ forces allowed) forcetow_N = elastic_constant * delta_towlength_m / towlength_new_m; # drag of tow-rope ( magic! ) var magic_constant = config.winch.magic_constant.getDoubleValue(); tow_drag_N = spoolspeed * spoolspeed * math.sqrt( math.sqrt( height * height ) * max_tow_length_m ) / magic_constant ; # mass = tow-mass only (drum-mass ignored) var mass_kg = max_tow_length_m * config.winch.weight_per_m_kg_m.getDoubleValue(); var acceleration = ( forcetow_N - tow_drag_N ) / mass_kg; var delta_spoolspeed = acceleration * delta_t; spoolspeed = spoolspeed - delta_spoolspeed; if ( spoolspeed < - unspool_max ) spoolspeed = - unspool_max; #if ( delta_towlength_m < 0. ) { # forcetow_N = 0.; #} } if ( forcetow_N > max_force_N ) { forcetow_N = max_force_N; var towlength_new_m = distance / ( forcetow_N / elastic_constant + 1. ); spoolspeed = (towlength_m - towlength_new_m ) / delta_t; } var power = forcetow_N * spoolspeed; if ( power > max_power_W) { power = max_power_W; forcetow_N = power / spoolspeed; towlength_new_m = towlength_m - spoolspeed * delta_t; } winch_hash.tow.length.setDoubleValue( towlength_new_m ); winch_hash.actual_spool_speed.setDoubleValue( spoolspeed ); winch_hash.actual_force.setDoubleValue( forcetow_N ); # force due to tow-weight (acts in tow direction at the heigher hitch) var force_due_to_weight_N = config.winch.weight_per_m_kg_m.getDoubleValue() * 9.81 * height; if (height < 0. ) force_due_to_weight_N = 0.; forcetow_N = forcetow_N + force_due_to_weight_N; var forcetow_LBS = forcetow_N * 0.224809; # N -> LBF if ( forcetow_N < breakforce_N ) { var distancepr = (myHitch_pos.distance_to(aiHitch_pos)); # correct a failure, if the projected length is larger than direct length if (distancepr > distance) { distancepr = distance;} var alpha = math.acos( (distancepr / distance) ); if ( aiHitch_pos.alt() > myHitch_pos.alt()) alpha = - alpha; var beta = ( aiHitchheadto - my_head_deg ) * D2R; var gamma = my_pitch_deg * D2R; var delta = my_roll_deg * D2R; var sina = math.sin(alpha); var cosa = math.cos(alpha); var sinb = math.sin(beta); var cosb = math.cos(beta); var sing = math.sin(gamma); var cosg = math.cos(gamma); var sind = math.sin(delta); var cosd = math.cos(delta); # calculate unit vector of force direction in JSBSim-system var force = 1; # global forces: alpha beta var fglobalx = force * cosa * cosb; var fglobaly = force * cosa * sinb; var fglobalz = force * sina; # local forces by pitch: gamma var flpitchx = fglobalx * cosg - fglobalz * sing; var flpitchy = fglobaly; var flpitchz = fglobalx * sing + fglobalz * cosg; # local forces by roll: delta var flrollx = flpitchx; var flrolly = flpitchy * cosd + flpitchz * sind; var flrollz = - flpitchy * sind + flpitchz * cosd; # asigning to LOCAL coord of plane var forcex = flrollx; var forcey = flrolly; var forcez = flrollz; # JSBSim-body-frame: x-> nose / y -> right wing / z -> down # apply forces to hook (forces are in LBS or N see above) winch_hash.apply_force.magnitude.setDoubleValue( forcetow_LBS ); winch_hash.apply_force.coord[0].setDoubleValue( forcex ); winch_hash.apply_force.coord[1].setDoubleValue( forcey ); winch_hash.apply_force.coord[2].setDoubleValue( forcez ); # check, if auto-release condition is reached var rope_angle_deg = math.atan2(forcez , forcex ) * R2D; #print("rope_angle_deg=",rope_angle_deg); if (rope_angle_deg > aircraft_settings.winch.automatic_release_angle.getDoubleValue() ) releaseWinch(); } # end force < break force else { # rope is broken winch_hash.broken.setBoolValue( 1 ); releaseWinch(); } if ( towlength_new_m > max_tow_length_m ) { write_message( "system", "tow length exceeded!" ); releaseWinch(); } } # end forces/JSBSim } # end hitch is closed (open == 0) } # end function winch # ###################################################################################################################### # create towrope # ###################################################################################################################### var createTowrope = func (device){ # create the towrope in the model property tree #print("createTowrope for ",device); if ( getprop("sim/hitches/" ~ device ~ "/rope/exist") == 0 ) { # does the towrope exist? # get the next free model id var freeModelid = getFreeModelID(); props.globals.getNode("sim/hitches/" ~ device ~ "/rope/model_id").setIntValue(freeModelid); props.globals.getNode("sim/hitches/" ~ device ~ "/rope/exist").setBoolValue(1); var towrope_ai = props.globals.getNode("ai/models/" ~ device ~ "rope", 1); var towrope_mod = props.globals.getNode("models", 1); towrope_ai.getNode("id", 1).setIntValue(4711); towrope_ai.getNode("callsign", 1).setValue("towrope"); towrope_ai.getNode("valid", 1).setBoolValue(1); towrope_ai.getNode("position/latitude-deg", 1).setValue(0.); towrope_ai.getNode("position/longitude-deg", 1).setValue(0.); towrope_ai.getNode("position/altitude-ft", 1).setValue(0.); towrope_ai.getNode("orientation/true-heading-deg", 1).setValue(0.); towrope_ai.getNode("orientation/pitch-deg", 1).setValue(0.); towrope_ai.getNode("orientation/roll-deg", 1).setValue(0.); towrope_mod.model = towrope_mod.getChild("model", freeModelid, 1); towrope_mod.model.getNode("path", 1).setValue(getprop("sim/hitches/" ~ device ~ "/rope/path_to_model") ); towrope_mod.model.getNode("longitude-deg-prop", 1).setValue("ai/models/" ~ device ~ "rope/position/longitude-deg"); towrope_mod.model.getNode("latitude-deg-prop", 1).setValue("ai/models/" ~ device ~ "rope/position/latitude-deg"); towrope_mod.model.getNode("elevation-ft-prop", 1).setValue("ai/models/" ~ device ~ "rope/position/altitude-ft"); towrope_mod.model.getNode("heading-deg-prop", 1).setValue("ai/models/" ~ device ~ "rope/orientation/true-heading-deg"); towrope_mod.model.getNode("roll-deg-prop", 1).setValue("ai/models/" ~ device ~ "rope/orientation/roll-deg"); towrope_mod.model.getNode("pitch-deg-prop", 1).setValue("ai/models/" ~ device ~ "rope/orientation/pitch-deg"); towrope_mod.model.getNode("load", 1).remove(); if( device == "winch" ){ winch_hash.rope.lat= check_or_create("ai/models/winchrope/position/latitude-deg", 0.0, "DOUBLE"); winch_hash.rope.lon= check_or_create("ai/models/winchrope/position/longitude-deg", 0.0, "DOUBLE"); winch_hash.rope.alt= check_or_create("ai/models/winchrope/position/altitude-ft", 0.0, "DOUBLE"); winch_hash.rope.hdg= check_or_create("ai/models/winchrope/orientation/true-heading-deg", 0.0, "DOUBLE"); winch_hash.rope.pitch= check_or_create("ai/models/winchrope/orientation/pitch-deg", 0.0, "DOUBLE"); } } # end towrope exist } # ###################################################################################################################### # get the next free id of "models/model" members # ###################################################################################################################### var getFreeModelID = func { #print("getFreeModelID"); var modelid = 0; # next unused id modelobjects = props.globals.getNode("models", 1).getChildren(); foreach ( var member; modelobjects ) { if ( (var c = member.getIndex()) != nil) { modelid = c + 1; } } #print("modelid=",modelid); return(modelid); } # ###################################################################################################################### # close aerotow hitch # ###################################################################################################################### var closeHitch = func { #print("closeHitch"); # close only, if # - not yet closed # - connected to property-node # - distance < towrope length if ( !aerotow_hash.open.getBoolValue() ) return; var aiNodeID = getprop("sim/hitches/aerotow/tow/connected-to-ai-or-mp-id"); # id of former found ai/mp aircraft if ( aiNodeID < 1 ) { setprop("sim/messages/atc", sprintf("No aircraft selected!")); return; } ##################################### calc distance between hitches ###################### ###################### my hitch position ####################### myPosition = geo.aircraft_position(); var my_head_deg = getprop("orientation/heading-deg"); var my_roll_deg = getprop("orientation/roll-deg"); var my_pitch_deg = getprop("orientation/pitch-deg"); # hook coordinates in Yasim-system (x-> nose / y -> left wing / z -> up) assignHitchLocations("aerotow"); var x = aircraft_settings.aerotow.local_pos[0].getDoubleValue(); var y = aircraft_settings.aerotow.local_pos[1].getDoubleValue(); var z = aircraft_settings.aerotow.local_pos[2].getDoubleValue(); #var x = getprop("sim/hitches/aerotow/local-pos-x"); #var y = getprop("sim/hitches/aerotow/local-pos-y"); #var z = getprop("sim/hitches/aerotow/local-pos-z"); var alpha_deg = my_roll_deg * (1.); # roll clockwise (looking in x-direction) := + var beta_deg = my_pitch_deg * (-1.); # pitch clockwise (looking in y-direction) := - # transform hook coordinates var Xn = PointRotate3D(x:x,y:y,z:z,xr:0.,yr:0.,zr:0.,alpha_deg:alpha_deg,beta_deg:beta_deg,gamma_deg:0.); var install_distance_m = Xn[0]; # in front of ref-point of glider var install_side_m = Xn[1]; var install_alt_m = Xn[2]; var myHitch_pos = myPosition.apply_course_distance( my_head_deg , install_distance_m ); var myHitch_pos = myPosition.apply_course_distance( my_head_deg - 90. , install_side_m ); myHitch_pos.set_alt(myPosition.alt() + install_alt_m); ###################### ai hitch position ####################### var found = 0; aiobjects = props.globals.getNode("ai/models").getChildren(); foreach (var aimember; aiobjects) { if ( (var c = aimember.getNode("id") ) != nil ) { var testprop = c.getValue(); if ( testprop == aiNodeID) { found = found + 1; # get coordinates var ai_lat = aimember.getNode("position/latitude-deg").getValue(); var ai_lon = aimember.getNode("position/longitude-deg").getValue(); var ai_alt = (aimember.getNode("position/altitude-ft").getValue()) * FT2M; var ai_pitch_deg = aimember.getNode("orientation/pitch-deg").getValue(); var ai_roll_deg = aimember.getNode("orientation/roll-deg").getValue(); var ai_head_deg = aimember.getNode("orientation/true-heading-deg").getValue(); var aiHitchX = aimember.getNode("sim/hitches/aerotow/local-pos-x").getValue(); var aiHitchY = aimember.getNode("sim/hitches/aerotow/local-pos-y").getValue(); var aiHitchZ = aimember.getNode("sim/hitches/aerotow/local-pos-z").getValue(); var aiPosition = geo.Coord.set_latlon( ai_lat, ai_lon, ai_alt ); var alpha_deg = ai_roll_deg * (1.); var beta_deg = ai_pitch_deg * (-1.); # transform hook coordinates var Xn = PointRotate3D(x:aiHitchX,y:aiHitchY,z:aiHitchZ,xr:0.,yr:0.,zr:0.,alpha_deg:alpha_deg,beta_deg:beta_deg,gamma_deg:0.); var install_distance_m = Xn[0]; # in front of ref-point of glider var install_side_m = Xn[1]; var install_alt_m = Xn[2]; var aiHitch_pos = aiPosition.apply_course_distance( ai_head_deg , install_distance_m ); var aiHitch_pos = aiPosition.apply_course_distance( ai_head_deg - 90. , install_side_m ); aiHitch_pos.set_alt(aiPosition.alt() + install_alt_m); var distance = (myHitch_pos.direct_distance_to(aiHitch_pos)); var towlength_m = props.globals.getNode("sim/hitches/aerotow/tow/length").getValue(); if ( distance > towlength_m ) { var aicallsign = getprop("sim/hitches/aerotow/tow/connected-to-ai-or-mp-callsign"); #setprop("sim/messages/atc", sprintf("Aircraft with callsign %s is too far away (distance is %4.0f meter).",aicallsign, distance)); setprop("sim/messages/atc", sprintf("Selected aircraft is too far away (distance to %s is %4.0f meter).",aicallsign, distance)); return; } setprop("sim/hitches/aerotow/tow/dist", distance); } } } aerotow_hash.open.setBoolValue( 0 ); setprop("sim/hitches/aerotow/mp_oldOpen", "true"); } # End function closeHitch # ###################################################################################################################### # release hitch # ###################################################################################################################### var releaseHitch = func (device){ #print("releaseHitch"); if ( fdm == "yasim" ) return; # bypass this routine for Yasim-aircraft setprop("sim/hitches/" ~ device ~ "/open", "true"); var hitchname = getprop("sim/hitches/" ~ device ~ "/force_name_jsbsim"); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/magnitude", 0.); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/x", 0.); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/y", 0.); setprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/z", 0.); if ( device == "aerotow" ) { setprop("sim/hitches/aerotow/tow/end-force-x", 0.); # MP tow-end forces setprop("sim/hitches/aerotow/tow/end-force-y", 0.); # setprop("sim/hitches/aerotow/tow/end-force-z", 0.); # } else { # Put automatic winch driver in after-tow phase phase = 5; } } # End function releaseHitch # ###################################################################################################################### # remove/delete towrope # ###################################################################################################################### var removeTowrope = func (device){ # remove the towrope from the property tree ai/models # remove the towrope from the property tree models/ if ( getprop("sim/hitches/" ~ device ~ "/rope/exist") == 1 ) { # does the towrope exist? # remove 3d model from scenery # identification is /models/model[x] with x=id_model var id_model = getprop("sim/hitches/" ~ device ~ "/rope/model_id"); var modelsNode = "models/model[" ~ id_model ~ "]"; props.globals.getNode(modelsNode).remove(); props.globals.getNode("ai/models/" ~ device ~ "rope").remove(); #print("towrope removed"); setprop("sim/hitches/" ~ device ~ "/rope/exist", 0); } } # ###################################################################################################################### # pull in towrope after hitch has been opened # ###################################################################################################################### var sink_rate_mps = 0.0; var pull_in_rope = func { if ( winch_hash.open.getBoolValue() ) { var current_tow_length = winch_hash.tow.length.getDoubleValue(); if ( current_tow_length > 15.0 ) { var dt = delta_time.getDoubleValue(); # get position of rope end (former myHitch_pos) var tow_lat = winch_hash.rope.lat.getDoubleValue(); var tow_lon = winch_hash.rope.lon.getDoubleValue(); var tow_alt_m = winch_hash.rope.alt.getDoubleValue() * FT2M; # get pitch and heading of rope var tow_heading_deg = winch_hash.rope.hdg.getDoubleValue(); var tow_pitch_rad = winch_hash.rope.pitch.getDoubleValue() * D2R; var aiTow_pos = geo.Coord.set_latlon( tow_lat, tow_lon, tow_alt_m ); var delta_length_m = config.winch.max_spool_speed.getDoubleValue() * dt; var delta_distance_m = delta_length_m * math.cos(tow_pitch_rad); aiTow_pos = aiTow_pos.apply_course_distance( tow_heading_deg , delta_distance_m ); # Wind influence aiTow_pos = aiTow_pos.apply_course_distance( wind_from.getDoubleValue() - 180, wind_speed_kt.getDoubleValue() * KT2MPS * dt ); var elevation = geo.elevation( aiTow_pos.lat(), aiTow_pos.lon() ); if( elevation == nil ) elevation = tow_alt_m; if( tow_alt_m > elevation ){ var delta_alt_m = delta_length_m * math.sin(tow_pitch_rad); # Calculate vertical sink rate # assume: drag is exclusively due to the chute, chute size is about 3.14m2 (circle radius 1m), rho is static at 1.29kg/m3 var mass_kg = current_tow_length * config.winch.weight_per_m_kg_m.getDoubleValue(); sink_rate_mps = sink_rate_mps + dt * ( 9.81 - 1.29 * math.pow( sink_rate_mps, 2 ) * 3.14 / mass_kg ); delta_alt_m = delta_alt_m - sink_rate_mps * dt; aiTow_pos.set_alt( tow_alt_m + delta_alt_m ); } else { aiTow_pos.set_alt( elevation ); } #print("aiTow_pos.alt()= ",aiTow_pos.alt()," ",tow_alt_m + delta_alt_m); # Winch position var winch_global_pos_x = winch_hash.global_pos[0].getDoubleValue(); var winch_global_pos_y = winch_hash.global_pos[1].getDoubleValue(); var winch_global_pos_z = winch_hash.global_pos[2].getDoubleValue(); var winch_geod = carttogeod(winch_global_pos_x, winch_global_pos_y, winch_global_pos_z); var aiWinch_pos = geo.Coord.new(); aiWinch_pos.set_latlon( winch_geod[0], winch_geod[1], winch_geod[2] ); var new_tow_length_m = aiTow_pos.direct_distance_to(aiWinch_pos); var aiHitchpitchto = -math.asin( (aiTow_pos.alt()-aiWinch_pos.alt()) / new_tow_length_m ) * R2D; # update position of rope #print("pull in active"); winch_hash.rope.lat.setDoubleValue( aiTow_pos.lat() ); winch_hash.rope.lon.setDoubleValue( aiTow_pos.lon() ); winch_hash.rope.alt.setDoubleValue( aiTow_pos.alt() * M2FT ); # update orientation of rope winch_hash.rope.hdg.setDoubleValue( aiTow_pos.course_to( aiWinch_pos ) ); winch_hash.rope.pitch.setDoubleValue( aiHitchpitchto ); # update length of rope winch_hash.tow.length.setDoubleValue( new_tow_length_m ); winch_hash.tow.dist.setDoubleValue( new_tow_length_m ); settimer( pull_in_rope , 0 ); } # end towlength > min else { #print("pull in finished!"); winch_hash.actual_spool_speed.setDoubleValue( 0 ); removeTowrope("winch"); # remove towrope model } } # end if open } # ###################################################################################################################### # set some AI-object default values # ###################################################################################################################### var setAIObjectDefaults = func (){ # set some default variables, needed to identify, if the found object is an AI-object, a "non-interactiv MP-object or # an interactive MP-object var aiNodeID = aerotow_hash.tow.conn_ai_or_mp_id.getIntValue(); # id of former found ai/mp aircraft aiobjects = props.globals.getNode("ai/models").getChildren(); foreach (var aimember; aiobjects) { if ( (var c = aimember.getNode("id") ) != nil ) { var testprop = c.getValue(); if ( testprop == aiNodeID) { # Set some dummy values. In case of an "interactive"-MP plane # the correct values will be transmitted in the following loop. # Create this variables if not present. aimember.getNode("sim/hitches/aerotow/local-pos-x",1).setValue(-5.); aimember.getNode("sim/hitches/aerotow/local-pos-y",1).setValue(0.); aimember.getNode("sim/hitches/aerotow/local-pos-z",1).setValue(0.); aimember.getNode("sim/hitches/aerotow/tow/dist",1).setValue(-1.); } } } } # ###################################################################################################################### # place winch model # ###################################################################################################################### var setWinchPositionAuto = func { # remove already existing winch model if ( getprop("/sim/hitches/winch/winch/winch-model-index") != nil ) { var id_model = getprop("/sim/hitches/winch/winch/winch-model-index"); var modelsNode = "models/model[" ~ id_model ~ "]"; props.globals.getNode(modelsNode).remove(); #print("winch model removed"); } var initial_length_m = config.winch.initial_tow_length.getDoubleValue(); var ac_pos = geo.aircraft_position(); # get position of aircraft var ac_hd = orientation[0].getDoubleValue(); # get heading of aircraft # setup winch # get initial runway position var ipos_lat_deg = getprop("sim/presets/latitude-deg"); var ipos_lon_deg = getprop("sim/presets/longitude-deg"); var ipos_hd_deg = getprop("sim/presets/heading-deg"); var ipos_alt_m = geo.elevation(ipos_lat_deg,ipos_lon_deg); var ipos_geo = geo.Coord.new().set_latlon(ipos_lat_deg, ipos_lon_deg, ipos_alt_m); # offset to initial position var deviation = (ac_pos.distance_to(ipos_geo)); # if deviation is too much, locate winch in front of glider, otherwise locate winch to end of runway if ( deviation > 200) { var w = ac_pos.apply_course_distance( ac_hd , initial_length_m -1. ); } else { var w = ipos_geo.apply_course_distance( ipos_hd_deg , initial_length_m - 1. ); } var wpalt = geo.elevation(w.lat(), w.lon()); w.set_alt(wpalt); var winchModel = geo.put_model("Models/Airport/supacat_winch.xml", w.lat(), w.lon(), (w.alt()+0.81), (w.course_to(ac_pos) )); winch_hash.global_pos[0].setDoubleValue( w.x() ); winch_hash.global_pos[1].setDoubleValue( w.y() ); winch_hash.global_pos[2].setDoubleValue( w.z() ); winch_hash.tow.dist.setDoubleValue( initial_length_m - 1.0 ); winch_hash.tow.length.setDoubleValue( initial_length_m ); #print("name=",winchModel.getName()," Index=",winchModel.getIndex()," Type=",winchModel.getType() ); #print("val=",winchModel.getValue()," children=",winchModel.getChildren()," size=",size(winchModel) ); setprop("/sim/hitches/winch/winch/winch-model-index",winchModel.getIndex() ); winch_hash.rope_breakage = winch_hash.rope_breakage_p.getBoolValue(); if( winch_hash.rope_breakage ){ if( config.winch.rope_breakage.random.getBoolValue() ){ winch_hash.rope_breakage_height_int = 1 + rand() * 300; } else { winch_hash.rope_breakage_height_int = config.winch.rope_breakage.height.getDoubleValue(); } } winch_hash.loss_of_power.enabled = winch_hash.loss_of_power.enabled_p.getBoolValue(); if( winch_hash.loss_of_power.enabled ){ if( config.winch.loss_of_power.random.getBoolValue() ){ winch_hash.loss_of_power.height_int = 1 + rand() * 300; } else { winch_hash.loss_of_power.height_int = config.winch.loss_of_power.height.getDoubleValue(); } } write_message( "pilot", "Connected to winch!" ); winch_hash.open.setBoolValue( 0 ); } # End function setWinchPositionAuto # ###################################################################################################################### # clutch / un-clutch winch # ###################################################################################################################### var runWinch = func { if ( !winch_hash.clutched.getBoolValue() ) { winch_hash.clutched.setBoolValue( 1 ); write_message( "pilot", "Winch clutched!" ); if( alt_agl_ft.getDoubleValue() < 10.0 ){ auto_winch_driver_init(); auto_winch_driver_update.restart( 0.0 ); } } else { winch_hash.clutched.setBoolValue( 0 ); write_message( "pilot", "Winch un-clutched!" ); } } # End function runWinch # ###################################################################################################################### # release winch # ###################################################################################################################### var releaseWinch = func { winch_hash.open.setBoolValue( 1 ); } # End function releaseWinch # ###################################################################################################################### # assignHitchLocations # ###################################################################################################################### var assignHitchLocations = func (device){ if ( fdm == "yasim" ) return; # bypass this routine for Yasim-aircraft if ( getprop("sim/hitches/" ~ device ~ "/decoupled-force-and-rope-locations") ) return; # bypass this routine #print("assignHitchLocations"); var hitchname = getprop("sim/hitches/" ~ device ~ "/force_name_jsbsim"); # location-x(yz)-in: JSBSim Structural Frame: x points to tail, y points to right wing, z points upward # local-pos-x(yz): YaSim frame: x points to nose, y points to left wing, z points upward setprop("sim/hitches/" ~ device ~ "/local-pos-x", - getprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/location-x-in") * IN2M ); setprop("sim/hitches/" ~ device ~ "/local-pos-y", - getprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/location-y-in") * IN2M ); setprop("sim/hitches/" ~ device ~ "/local-pos-z", getprop("fdm/jsbsim/external_reactions/" ~ hitchname ~ "/location-z-in") * IN2M ); } # End function assignHitchLocations # ###################################################################################################################### # point transformation # ###################################################################################################################### var PointRotate3D = func (x,y,z,xr,yr,zr,alpha_deg,beta_deg,gamma_deg){ # --------------------------------------------------------------------------------- # rotates point (x,y,z) about all 3 cartesian axis # center of rotation (xr,yr,zr) # angle of rotation about x-axis = alpha # angle of rotation about y-axis = beta # angle of rotation about z-axis = gamma # delivers new point coordinates (x_new,y_new,z_new) # --------------------------------------------------------------------------------- # # # Definitions: # ---------------- # # x y z # alpha beta gamma # # # z # | y # | / # |/ # ----->x # #---------------------------------------------------------------------------------- # Transformation in rotation-system X_rel = X-Xr = (x-xr, y-yr, z-zr) var x_rel = x-xr; var y_rel = y-yr; var z_rel = z-zr; # Trigonometry var alpha_rad = D2R * alpha_deg; var beta_rad = D2R * beta_deg; var gamma_rad = D2R * gamma_deg; var sin_alpha = math.sin(alpha_rad); var cos_alpha = math.cos(alpha_rad); var sin_beta = math.sin(beta_rad); var cos_beta = math.cos(beta_rad); var sin_gamma = math.sin(gamma_rad); var cos_gamma = math.cos(gamma_rad); # Matrices # # Rotate about x-axis Rx(alpha) # # Rx11 Rx12 Rx13 1 0 0 # Rx(alpha)= Rx21 Rx22 Rx23 = 0 cos(alpha) -sin(alpha) # Rx31 Rx32 Rx33 0 sin(alpha) cos(alpha) # var Rx11 = 1.; var Rx12 = 0.; var Rx13 = 0.; var Rx21 = 0.; var Rx22 = cos_alpha; var Rx23 = - sin_alpha; var Rx31 = 0.; var Rx32 = sin_alpha; var Rx33 = cos_alpha; # # Rotate about y-axis Ry(beta) # # Ry11 Ry12 Ry13 cos(beta) 0 sin(beta) # Ry(beta)= Ry21 Ry22 Ry23 = 0 1 0 # Ry31 Ry32 Ry33 -sin(beta) 0 cos(beta) # var Ry11 = cos_beta; var Ry12 = 0.; var Ry13 = sin_beta; var Ry21 = 0.; var Ry22 = 1.; var Ry23 = 0.; var Ry31 = - sin_beta; var Ry32 = 0.; var Ry33 = cos_beta; # # Rotate about z-axis Rz(gamma) # # Rz11 Rz12 Rz13 cos(gamma) -sin(gamma) 0 # Rz(gamma)= Rz21 Rz22 Rz23 = sin(gamma) cos(gamma) 0 # Rz31 Rz32 Rz33 0 0 1 # var Rz11 = cos_gamma; var Rz12 = - sin_gamma; var Rz13 = 0.; var Rz21 = sin_gamma; var Rz22 = cos_gamma; var Rz23 = 0.; var Rz31 = 0.; var Rz32 = 0.; var Rz33 = 1.; # # First rotation about x-axis # X_x = Rx*X_rel var x_x = Rx11 * x_rel + Rx12 * y_rel + Rx13 * z_rel; var y_x = Rx21 * x_rel + Rx22 * y_rel + Rx23 * z_rel; var z_x = Rx31 * x_rel + Rx32 * y_rel + Rx33 * z_rel; # # subsequent rotation about y-axis # X_xy = Ry*X_x var x_xy = Ry11 * x_x + Ry12 * y_x + Ry13 * z_x; var y_xy = Ry21 * x_x + Ry22 * y_x + Ry23 * z_x; var z_xy = Ry31 * x_x + Ry32 * y_x + Ry33 * z_x; # # subsequent rotation about z-axis: # X_xyz = Rz*X_xy var x_xyz = Rz11 * x_xy + Rz12 * y_xy + Rz13 * z_xy; var y_xyz = Rz21 * x_xy + Rz22 * y_xy + Rz23 * z_xy; var z_xyz = Rz31 * x_xy + Rz32 * y_xy + Rz33 * z_xy; # Back transformation X_rel = X-Xr = (x-xr, y-yr, z-zr) var xn = xr + x_xyz; var yn = yr + y_xyz; var zn = zr + z_xyz; var Xn = [xn,yn,zn]; return Xn; } var groundspeed_kt = props.globals.getNode("velocities/groundspeed-kt"); var alt_agl_ft = props.globals.getNode("position/altitude-agl-ft"); var weight_lb = nil; var empty_weight_lb = nil; var ls = setlistener("/sim/signals/fdm-initialized", func() { if( fdm == "jsb" ) { weight_lb = props.globals.getNode("fdm/jsbsim/inertia/weight-lbs"); empty_weight_lb = props.globals.getNode("fdm/jsbsim/inertia/empty-weight-lbs"); } else if ( fdm == "yasim" ) { weight_lb = props.globals.getNode("yasim/gross-weight-lbs"); } check_aircraft_tow_settings(); removelistener(ls); }); var winch_target = 0.8; var winch_throttle = 0.0; var winch_idle_force = 1000; # N var winch_idle_speed = 4; # mps var acceleration_N_s = nil; var acceleration_m_s_s = nil; var phase = 0; var alpha = 0.0; var requested_change = 0.0; # positive: request to speed up, negative: request to slow down, 0: auto speed ########### ## AUTOMATIC WINCH DRIVER ########### # Phase Action description Triggered by # 0 slowly pull rope until tight (currently omitted) runWinch() # 1 winch running on idle until aircraft is moving "rope tight" command # 2 winch accelerating to pre-defined RPM "ready" command # 3 glider in air, winch adjusts RPM based on angular velocity and faster/slower commands by pilot "clear of ground" command # 4 winch slowly decelerates to idle aircraft 10-20 deg from automatic release angle # 5 winch pulls in remaining rope "rope released" command ## Calculation of predefined targets: # -weight factor, accounts for additional weight (second pilot, water ballast) # -wind factors, account for different tow speed due to headwind/tailwind and crosswind var target_force = 0.0; # N var target_speed = 0.0; # m/s var auto_winch_driver_init = func () { target_force = aircraft_settings.winch.typical_tow_force.getDoubleValue(); target_speed = aircraft_settings.winch.typical_tow_speed.getDoubleValue() / 3.6; # convert kph to mps # Adjust target speed and force for current wind (increase for tail- and crosswind, reduce for headwind) # define wind frame: expect from 15 kts tailwind to 15 kts headwind -> translates to a change of +- 0.1 var launch_heading_deg = winch_hash.rope.hdg.getDoubleValue(); var wind_from_heading = wind_from.getDoubleValue(); var wind_speed = wind_speed_kt.getDoubleValue(); # calculate linear component var rel_wind_heading = launch_heading_deg - wind_from_heading; var wind_linear = wind_speed * math.cos( rel_wind_heading * D2R ); # negative values mean tailwind var wind_normal = wind_speed * math.cos( -( 90 - rel_wind_heading ) * D2R ); # negative values mean wind from the left # compensate for tail/headwind var wind_linear_add = math.clamp( -wind_linear / 150, -0.1, 0.1 ); # compensate for crosswind var wind_normal_add = math.min( math.abs( wind_normal ) / 150, 0.1 ); # apply compensation to winch driver variables target_force += ( wind_linear_add + wind_normal_add ) * config.winch.max_force.getDoubleValue(); target_speed += ( wind_linear_add + wind_normal_add ) * config.winch.max_spool_speed.getDoubleValue(); # set acceleration/deceleration variables acceleration_N_s = config.winch.force_acceleration.getDoubleValue(); acceleration_m_s_s = config.winch.spool_acceleration.getDoubleValue(); # put winch driver into initial state phase = 0; } var my_pos = nil; var winch_pos = geo.Coord.new(); var auto_winch_driver = func () { if( !winch_hash.clutched.getBoolValue() ) return; var dt = delta_time.getDoubleValue(); if( winch_hash.type != 0 and winch_hash.type != 1 ) { die("(towing system): FATAL: unsupported winch type!"); } if( phase == 3 or phase == 4 ){ my_pos = geo.aircraft_position(); # Winch position var winch_global_pos_x = winch_hash.global_pos[0].getDoubleValue(); var winch_global_pos_y = winch_hash.global_pos[1].getDoubleValue(); var winch_global_pos_z = winch_hash.global_pos[2].getDoubleValue(); var winch_geod = carttogeod(winch_global_pos_x, winch_global_pos_y, winch_global_pos_z); winch_pos.set_latlon( winch_geod[0], winch_geod[1], winch_geod[2] ); alpha = math.asin( ( my_pos.alt() - winch_pos.alt() ) / winch_pos.direct_distance_to( my_pos ) ) * R2D; } if( phase == 0 ) { phase = 1; } if( phase == 1 ) { if( winch_hash.type == 0 ){ force_setting = winch_idle_force; } else { speed_setting = winch_idle_speed; } # Condition for going into the next phase if( groundspeed_kt.getDoubleValue() > 1.0 ) { phase = 2; write_message( "launch-signaller", "Ready!" ); write_message( "winch-driver", "Ready"); } } else if ( phase == 2 or phase == 2.5 ){ # Accelerate to target value if( winch_hash.type == 0 ){ if( force_setting < target_force ){ force_setting += acceleration_N_s * dt; } else { force_setting = target_force; if( phase == 2.5 ){ phase = 3; } } } else { if( speed_setting < target_speed ){ speed_setting += acceleration_m_s_s * dt; } else { speed_setting = target_speed; if( phase == 2.5 ){ phase = 3; } } } # Condition for going into the next phase if( alt_agl_ft.getDoubleValue() > 10 and phase < 2.5 ){ phase = 2.5; # stay here until we have reached the target value write_message( "launch-signaller", "Clear of ground!" ); write_message( "winch-driver", "Clear"); } } else if ( phase == 3 ){ if( requested_change != 0 ){ if( winch_hash.type == 0 ){ force_setting += requested_change * config.winch.max_force.getDoubleValue() * dt; } else { speed_setting += requested_change * config.winch.max_spool_speed.getDoubleValue() * dt; } requested_change -= requested_change * dt; if( math.abs( requested_change ) < dt ){ requested_change = 0.0; } } var thr_speed = 0.8; var thr_angle = 0.5; if( winch_hash.type == 0 ){ var frac_speed = math.clamp( 0.2 + 1 - ( 1 / ( 1 - thr_speed ) ) * ( ( winch_hash.actual_spool_speed.getDoubleValue() / config.winch.max_spool_speed.getDoubleValue() ) - thr_speed ) * 0.8, 0.0, 1.0 ); var frac_angle = math.clamp( 0.2 + 1 - ( 1 / ( 1 - thr_angle ) ) * ( ( alpha / ( aircraft_settings.winch.automatic_release_angle.getDoubleValue() - 30 ) ) - thr_angle ) * 0.8, 0.0, 1.0 ); force_setting = target_force * frac_speed * frac_angle; } elsif( winch_hash.type == 1 ){ var frac_speed = math.clamp( 0.2 + 1 - ( 1 / ( 1 - thr_speed ) ) * ( ( winch_hash.actual_spool_speed.getDoubleValue() / config.winch.max_spool_speed.getDoubleValue() ) - thr_speed ) * 0.8, 0.0, 1.0 ); var frac_angle = math.clamp( 0.2 + 1 - ( 1 / ( 1 - thr_angle ) ) * ( ( alpha / ( aircraft_settings.winch.automatic_release_angle.getDoubleValue() - 30 ) ) - thr_angle ) * 0.8, 0.0, 1.0 ); speed_setting = target_speed * frac_speed * frac_angle; } if( ( alpha >= ( aircraft_settings.winch.automatic_release_angle.getDoubleValue() - 10 ) ) ) { phase = 4; } } else if ( phase == 4 ){ # Reduce values back to idle if( speed_setting > winch_idle_speed ){ var deceleration = 3 * math.clamp( ( speed_setting - winch_idle_speed ) / ( aircraft_settings.winch.automatic_release_angle.getDoubleValue() - 2 - alpha ), 5, acceleration_m_s_s ); speed_setting -= deceleration * dt; } else { speed_setting = winch_idle_speed; } # Condition for going into the next phase is set by the releaseHitch() command } else if ( phase == 5 ){ if( winch_hash.type == 0 ){ force_setting = winch_idle_force; } else { speed_setting = winch_idle_speed; } auto_winch_driver_update.stop(); } } var auto_winch_driver_update = maketimer( 0.0, auto_winch_driver ); auto_winch_driver_update.simulatedTime = 1; var winch_faster = func ( d = 0.1 ) { write_message( "pilot", "Faster!"); # Skip acceleration to pre-set target force/speed in case of pilot command if( phase == 2.5 ){ phase = 3; } if( phase != 3 ){ write_message( "winch-driver", "Unable!"); return; } if( ( winch_hash.type == 0 and ( force_setting + d * config.winch.max_force.getDoubleValue() ) <= config.winch.max_force.getDoubleValue() ) or ( winch_hash.type == 1 and ( speed_setting + d * config.winch.max_spool_speed.getDoubleValue() ) <= config.winch.max_spool_speed.getDoubleValue() ) ){ requested_change = d; write_message( "winch-driver", "Faster"); } else { if( winch_hash.type == 0 ){ force_setting = config.winch.max_force.getDoubleValue(); } else { speed_setting = config.winch.max_spool_speed.getDoubleValue(); } write_message( "winch-driver", "At maximum!"); } } var winch_slower = func ( d = 0.1 ) { write_message( "pilot", "Slower!"); # Skip acceleration to pre-set target force/speed in case of pilot command if( phase == 2.5 ){ phase = 3; } if( phase != 3 ){ write_message( "winch-driver", "Unable!"); return; } if( ( winch_hash.type == 0 and force_setting - d * config.winch.max_force.getDoubleValue() >= winch_idle_force ) or ( winch_hash.type == 1 and speed_setting - d * config.winch.max_spool_speed.getDoubleValue() >= winch_idle_speed ) ){ requested_change = -d; write_message( "winch-driver", "Slower"); } else { if( winch_hash.type == 0 ){ force_setting = winch_idle_force; } else { speed_setting = winch_idle_speed; } write_message( "winch-driver", "At minimum!"); } } ################################################################################################################################## # todo: # ------ # # - animate rope slack # - dynamic ID for ai-rope-model # # Please contact D_NXKT at yahoo.de for bug-reports, suggestions, ... #