# Manages the engine # # Fuel system: based on the Spitfire. Manages primer and negGCutoff # Hobbs meter # =============================== DEFINITIONS =========================================== # set the update period var UPDATE_PERIOD = 0.3; # =============================== Hobbs meter ======================================= # this property is saved by aircraft.timer var hobbsmeter_engine_160hp = aircraft.timer.new("/sim/time/hobbs/engine[0]", 60, 1); var hobbsmeter_engine_180hp = aircraft.timer.new("/sim/time/hobbs/engine[1]", 60, 1); var init_hobbs_meter = func(index, meter) { setlistener("/engines/engine[" ~ index ~ "]/running", func { if (getprop("/engines/engine[" ~ index ~ "]/running")) { meter.start(); print("Hobbs system started"); } else { meter.stop(); print("Hobbs system stopped"); } }, 1, 0); }; init_hobbs_meter(0, hobbsmeter_engine_160hp); init_hobbs_meter(1, hobbsmeter_engine_180hp); var update_hobbs_meter = func { # in seconds var hobbs_160hp = getprop("/sim/time/hobbs/engine[0]") or 0.0; var hobbs_180hp = getprop("/sim/time/hobbs/engine[1]") or 0.0; # This uses minutes, for testing #hobbs = hobbs / 60.0; # in hours #hobbs = (hobbs_160hp + hobbs_180hp) / 3600.0; # tenths of hour var hobbs = 0; var current_engine = getprop("controls/engines/active-engine"); if (current_engine == 0) { hobbs = hobbs_160hp / 3600.0; } else if (current_engine == 1) { hobbs = hobbs_180hp / 3600.0; } setprop("/instrumentation/hobbs-meter/digits0", math.mod(int(hobbs * 10), 10)); # rest of digits setprop("/instrumentation/hobbs-meter/digits1", math.mod(int(hobbs), 10)); setprop("/instrumentation/hobbs-meter/digits2", math.mod(int(hobbs / 10), 10)); setprop("/instrumentation/hobbs-meter/digits3", math.mod(int(hobbs / 100), 10)); setprop("/instrumentation/hobbs-meter/digits4", math.mod(int(hobbs / 1000), 10)); }; setlistener("/sim/time/hobbs/engine[0]", update_hobbs_meter, 1, 0); setlistener("/sim/time/hobbs/engine[1]", update_hobbs_meter, 1, 0); setlistener("/controls/engines/active-engine", update_hobbs_meter, 1, 0); # ========== primer stuff ====================== # Toggles the state of the primer var pumpPrimer = func { var push = getprop("/controls/engines/engine/primer-lever") or 0; if (push) { var pump = getprop("/controls/engines/engine/primer") or 0; setprop("/controls/engines/engine/primer", pump + 1); setprop("/controls/engines/engine/primer-lever", 0); } else { setprop("/controls/engines/engine/primer-lever", 1); } }; # Primes the engine automatically. This function takes several seconds var autoPrime = func { var p = getprop("/controls/engines/engine/primer") or 0; if (p < 3) { pumpPrimer(); settimer(autoPrime, 1); } }; # Mixture will be calculated using the primer during 5 seconds AFTER the pilot used the starter # This prevents the engine to start just after releasing the starter: the propeller will be running # thanks to the electric starter, but carburator has not yet enough mixture var primerTimer = maketimer(5, func { setprop("/controls/engines/engine/use-primer", 0); # Reset the number of times the pilot used the primer only AFTER using the starter setprop("/controls/engines/engine/primer", 0); print("Primer reset to 0"); primerTimer.stop(); }); # ========== oil consumption ====================== # Thanks to HHS81 (Benedikt Hallinger) for more advanced simulation var service_hours = getprop("/engines/active-engine/oil-service-hours"); var consumption_qps = 0.0; var rpm_factor = 0.0; var rpm = 0; var oil_level = 0; var oil_full = 0; var oil_lacking = 0; var oil_level_limited = 0; var service_hours_increase_qph = 0; var service_hours_new = 0; var low_oil_pressure_factor = 0.0; var low_oil_temperature_factor = 0.0; # ======= OIL SYSTEM INIT ======= if (!getprop("/engines/active-engine/oil-service-hours")) { setprop("/engines/active-engine/oil-service-hours", 0); } var oil_consumption = maketimer(1.0, func { oil_level = getprop("/engines/active-engine/oil-level"); if (getprop("/controls/engines/active-engine") == 0) oil_full = 7; if (getprop("/controls/engines/active-engine") == 1) oil_full = 8; oil_lacking = oil_full - oil_level; setprop("/engines/active-engine/oil-lacking", oil_lacking); if (getprop("/engines/active-engine/oil_consumption_allowed")) { rpm = getprop("/engines/active-engine/rpm"); # Quadratic formula which outputs 1.0 for input 2300 RPM (cruise value), # 0.6 for 700 RPM (idle) and 1.2 for 2700 RPM (max) rpm_factor = 0.00000012 * math.pow(rpm, 2) - 0.0001 * rpm + 0.62; # Consumption rate defined as 0.33 quarts per 1 hour (3600 seconds) (Lycoming Manual 3-6 p27) # at 2350 RPM (normal cruise) consumption_qps = 0.33 / 3600; # Raise consumption when oil level is > 8 quarts (blowout) if (oil_level > oil_full) { consumption_qps = consumption_qps * 1.3; } # Consumption also raises with oil in service time (lower viscosity => more friction) # (Oil should be changed at 50 hrs!) # See: http://www.t-craft.org/Reference/Aircraft.Oil.Usage.pdf # Hours: 0 | 10 | 25 | 50 | 75 # Add Qts/hr: 0 | 0.02 | 0.125 | 0.5 | 1.125 service_hours = getprop("/engines/active-engine/oil-service-hours"); service_hours_increase_qph = 0.00020 * math.pow(service_hours, 2); service_hours_increase_qph = std.min(1.5, service_hours_increase_qph); # limit increase to 1.5 (at which point you really should think of changing it) service_hours_increase_qps = service_hours_increase_qph / 3600; consumption_qps = consumption_qps + service_hours_increase_qps; if (getprop("/engines/active-engine/running")) { oil_level = oil_level - consumption_qps * rpm_factor; setprop("/engines/active-engine/oil-level", oil_level); setprop("/engines/active-engine/oil-consume-qps", consumption_qps); setprop("/engines/active-engine/oil-consume-qph", consumption_qps * 3600); service_hours_new = (service_hours * 3600 + 1) / 3600; # add one second service time setprop("/engines/active-engine/oil-service-hours", service_hours_new); } else { setprop("/engines/active-engine/oil-consume-qps", 0); } low_oil_pressure_factor = 1.0; low_oil_temperature_factor = 1.0; # If oil gets low (< 3.0), pressure should drop and temperature should rise oil_level_limited = std.min(oil_level, 3.0); # Should give 1.0 for oil_level = 3 and 0.1 for oil_level 1.97, # which is the min before the engine stops low_oil_pressure_factor = 0.873786408 * oil_level_limited - 1.621359224; # Should give 1.0 for oil_level = 3 and 1.5 for oil_level 1.97 low_oil_temperature_factor = -0.485436893 * oil_level_limited + 2.456310679; setprop("/engines/active-engine/low-oil-pressure-factor", low_oil_pressure_factor); setprop("/engines/active-engine/low-oil-temperature-factor", low_oil_temperature_factor); } else { # if oil consumption is not allowed, the oil level is set to full and pressure and temp factors are set to 1.0 if (getprop("/controls/engines/active-engine") == 0) setprop("/engines/active-engine/oil-level", 7); if (getprop("/controls/engines/active-engine") == 1) setprop("/engines/active-engine/oil-level", 8); setprop("/engines/active-engine/low-oil-pressure-factor", 1.0); setprop("/engines/active-engine/low-oil-temperature-factor", 1.0); } }); # Oil Refilling var oil_refill = func(){ var previous_oil_level = getprop("/engines/active-engine/oil-level"); var service_hours = getprop("/engines/active-engine/oil-service-hours"); var oil_level = getprop("/engines/active-engine/oil-level"); var refilled = oil_level - previous_oil_level; #print("OIL Refill init: svcHrs=", service_hours, "; oil_level=",oil_level, "; previous_oil_level=",previous_oil_level, "; refilled=",refilled); if (refilled >= 0) { # when refill occured, the new oil "makes the old oil younger" var pct = 0; if (oil_level > 0) { pct = previous_oil_level / oil_level; } var newService_hours = service_hours * pct; setprop("/engines/active-engine/oil-service-hours", newService_hours); #print("OIL Refill: pct=", pct, "; service_hours=",service_hours, "; newService_hours=", newService_hours, "; previous_oil_level=", previous_oil_level, "; oil_level=",oil_level); } previous_oil_level = oil_level; } # ======= Oil temperature jsbsim compensator ======= # Currently, jsbsim oil temperature always initializes at 60°F. # We want an temperature that initialize to environment temperature until first start # and then gradually switch over to the real jsbsim value after some time. var calculate_real_oiltemp = maketimer(0.5, func { if (!getprop("/engines/active-engine/already-started-in-session")) { # engine is still cold var temp_env = getprop("/environment/temperature-degf") or 60; var temp_jsbsim_oil = getprop("/engines/active-engine/oil-temperature-degf") or 60; current_temp_diff = temp_jsbsim_oil - temp_env; setprop("/engines/active-engine/oil-temperature-env-diff", current_temp_diff); } else { # engine has been started at least one time: # gradually remove the difference as jsbsim adapts to real environment temperature calculate_real_oiltemp.stop(); interpolate("/engines/active-engine/oil-temperature-env-diff", 0, 180); # hand over to jsbsim caluclation gradually over 2 minutes } }); # ========== carburetor icing ====================== var carb_icing_function = maketimer(1.0, func { if (getprop("/engines/active-engine/carb_icing_allowed")) { var rpm = getprop("/engines/active-engine/rpm"); var dewpointC = getprop("/environment/dewpoint-degc"); var dewpointF = dewpointC * 9.0 / 5.0 + 32; var airtempF = getprop("/environment/temperature-degf"); var oil_temp = getprop("/engines/active-engine/oil-temperature-degf"); var egt_degf = getprop("/engines/active-engine/egt-degf"); var engine_running = getprop("/engines/active-engine/running"); var carb_ice = getprop("/engines/active-engine/carb_ice"); # the formula below attempts to model the graph found in the POH which relates air temperature, dew point and RPM to icing # conditions. The outputs of carb_icing_formula ranges from 0.65 to -0.35 (positive means ice is accumulating, negative # means that ice is melting) var factorX = 13.2 - 3.2 * math.atan2 ( ((rpm - 2000.0) * 0.008), 1); var factorY = 7.0 - 2.0 * math.atan2 ( ((rpm - 2000.0) * 0.008), 1); var carb_icing_formula = (math.exp( math.pow((0.6 * airtempF + 0.3 * dewpointF - 42.0),2) / (-2 * math.pow(factorX,2))) * math.exp( math.pow((0.3 * airtempF - 0.6 * dewpointF + 14.0),2) / (-2 * math.pow(factorY,2))) - 0.35) * engine_running; # the efficacy of carb heat depends on the EGT. With a typical EGT of ~1500, the carb_heat_rate will be around -1.5. # This value is an educated guess of the RL effect, and should melt ice regardless of the icing rate if (getprop("/controls/engines/current-engine/carb-heat")) var carb_heat_rate = -0.001 * egt_degf; else var carb_heat_rate = 0.0; # a warm engine will accumulate less ice than a cold one, which is what oil temp factor is used for. oil_temp_factor # ranges from 0 to aprox -0.2 (at 250 oF). These values are educated guesses of the RL effect var oil_temp_factor = oil_temp / -1250; # the final rate of icing or melting is then calculated by all these effects together var carb_icing_rate = carb_icing_formula + carb_heat_rate + oil_temp_factor; # since the carb_icing_rate gives an arbitrary final value, the rate is then scaled down by 0.00001 to ensure ice # accumulates as slowly as expected carb_ice = carb_ice + carb_icing_rate * 0.00001; carb_ice = std.max(0.0, std.min(carb_ice, 1.0)); # this property is used to lower the RPM of the engine as ice accumulates (more ice in the carburator == less power) var vol_eff_factor = std.max(0.0, 0.85 - 1.72 * carb_ice); setprop("/engines/active-engine/carb_ice", carb_ice); setprop("/engines/active-engine/carb_icing_rate", carb_icing_rate); setprop("/engines/active-engine/volumetric-efficiency-factor", vol_eff_factor); setprop("/engines/active-engine/oil_temp_factor", oil_temp_factor); } else { setprop("/engines/active-engine/carb_ice", 0.0); setprop("/engines/active-engine/carb_icing_rate", 0.0); setprop("/engines/active-engine/volumetric-efficiency-factor", 0.85); setprop("/engines/active-engine/oil_temp_factor", 0.0); }; }); # ========== engine coughing ====================== var engine_coughing = func(){ var coughing = getprop("/engines/active-engine/coughing"); var running = getprop("/engines/active-engine/running"); if (coughing and running) { # the code below kills the engine and then brings it back to life after 0.25 seconds, simulating a cough setprop("/engines/active-engine/kill-engine", 1); settimer(func { setprop("/engines/active-engine/kill-engine", 0); }, 0.25); }; # basic value for the delay (interval between consecutive coughs), in case no fuel contamination nor carb ice are present var delay = 2; # if coughing due to fuel contamination, then cough interval depends on quantity of water var water_contamination0 = getprop("/consumables/fuel/tank[0]/water-contamination"); var water_contamination1 = getprop("/consumables/fuel/tank[1]/water-contamination"); var total_water_contamination = std.min((water_contamination0 + water_contamination1), 0.4); if (total_water_contamination > 0) { # if contamination is near 0, then interval is between 17 and 20 seconds, but if contamination is near the # engine stopping value of 0.4, then interval falls to around 0.5 and 3.5 seconds delay = 3.0 * rand() + 17 - 41.25 * total_water_contamination; }; # if coughing due to carb ice melting, then cough depends on quantity of ice in the carburettor var carb_ice = getprop("/engines/active-engine/carb_ice"); if (carb_ice > 0) { # if carb_ice is near 0, then interval is between 17 and 20 seconds, but if carb_ice is near the # engine stopping value of 0.3, then interval falls to around 0.5 and 3.5 seconds delay = 3.0 * rand() + 17 - 41.25 * carb_ice; }; coughing_timer.restart(delay); } var coughing_timer = maketimer(1, engine_coughing); # ====== Engine starting actions ====== var engine_starting = props.globals.initNode("/engines/engine/starting", 0, "BOOL"); setlistener("/engines/engine/running", func(ngn){ if (ngn.getValue() and !getprop("/engines/engine[0]/coughing")) { engine_starting.setValue(1); var timer = maketimer(1, func(){ engine_starting.setValue(0); }); timer.singleShot = 1; # timer will only be run once timer.start(); } else { engine_starting.setValue(0); } },0,0); setlistener("/engines/engine/starting", func(ngn){ # Eye-candy: when engine starts, let the view shake a bit if (ngn.getValue() and getprop("/sim/current-view/internal")) { var curX = getprop("/sim/current-view/x-offset-m"); var xtimer = maketimer(0.05, func(){ interpolate("/sim/current-view/x-offset-m", curX-0.0015+rand()*0.003, 0.05); }); xtimer.start(); var curY = getprop("/sim/current-view/y-offset-m"); var ytimer = maketimer(0.05, func(){ interpolate("/sim/current-view/y-offset-m", curY-0.0015+rand()*0.003, 0.05); }); ytimer.start(); var stoptimer = maketimer(0.8, func(){ xtimer.stop(); ytimer.stop(); interpolate("/sim/current-view/x-offset-m", curX, 0.1); interpolate("/sim/current-view/y-offset-m", curY, 0.1); }); stoptimer.singleShot = 1; stoptimer.start(); } }, 0, 0); # ========== Main loop ====================== var update = func { #this block should be moved out of nasal and into jsbsim or autopilot logic var leftTankUsable = 0; var rightTankUsable = 0; if (getprop("/consumables/fuel/tank[0]/selected") and getprop("/consumables/fuel/tank[0]/level-gal_us") > 0) leftTankUsable = 1; if (getprop("/consumables/fuel/tank[1]/selected") and getprop("/consumables/fuel/tank[1]/level-gal_us") > 0) rightTankUsable = 1; if (getprop("/consumables/fuel/tank[2]/selected") and getprop("/consumables/fuel/tank[2]/level-gal_us") > 0) leftTankUsable = 1; if (getprop("/consumables/fuel/tank[3]/selected") and getprop("/consumables/fuel/tank[3]/level-gal_us") > 0) rightTankUsable = 1; var outOfFuel = !(leftTankUsable or rightTankUsable); # We use the mixture to control the engines, so set the mixture var usePrimer = getprop("/controls/engines/engine/use-primer") or 0; if(getprop("/controls/panel/glass")) usePrimer = 0; var engine_running = getprop("/engines/active-engine/running"); if (outOfFuel and (engine_running or usePrimer)) { print("Out of fuel!"); gui.popupTip("Out of fuel!"); } elsif (usePrimer and !engine_running and getprop("/engines/active-engine/oil-temperature-degf") <= 75) { # Mixture is controlled by start conditions var primer = getprop("/controls/engines/engine/primer"); if (!getprop("/fdm/jsbsim/fcs/mixture-primer-cmd") and getprop("/controls/switches/starter") and getprop("/controls/switches/master-bat")) { if (primer < 3) { print("Use the primer!"); gui.popupTip("Use the primer!"); } elsif (primer > 6) { print("Flooded engine!"); gui.popupTip("Flooded engine!"); } else { print("Check the throttle!"); gui.popupTip("Check the throttle!"); } } } var active_engine = getprop("/controls/engines/active-engine"); var rpm = getprop("/engines/engine", active_engine, "rpm"); # sorry - had to hack this to prevent coughing on startup due to the oil pressure simulation. Maybe this can be used elsewhere if (rpm < 900 and getprop("/controls/switches/starter") == 1) { # make sure it is not triggered if you accidentally hit s in the air setprop("/engines/active-engine/ready-oil-press-checker", 1); # 0 = off, 1 = checker is armed, 2 = engine is running and ready } if (getprop("/engines/active-engine/ready-oil-press-checker") == 1 and getprop("/engines/active-engine/rpm") > 900) { setprop("/engines/active-engine/ready-oil-press-checker", 2); # engine is ready for use } }; setlistener("/controls/switches/starter", func { var v = getprop("/controls/switches/starter") or 0; if (v == 0) { print("Starter off"); # notice the starter will be reset after 5 seconds primerTimer.restart(5); } else { print("Starter on"); if(getprop("/controls/panel/glass")) setprop("/controls/engines/engine/use-primer", 0); else setprop("/controls/engines/engine/use-primer", 1); if (primerTimer.isRunning) { primerTimer.stop(); } } }, 1, 0); # ================================ Initalize ====================================== # Make sure all needed properties are present and accounted # for, and that they have sane default values. setprop("/engines/active-engine/rpm", 0); setprop("/engines/active-engine/ready-oil-press-checker", 0); # =============== Variables ================ controls.incThrottle = func { var delta = arg[1] * controls.THROTTLE_RATE * getprop("/sim/time/delta-realtime-sec"); var old_value = getprop("/controls/engines/current-engine/throttle"); var new_value = std.max(0.0, std.min(old_value + delta, 1.0)); setprop("/controls/engines/current-engine/throttle", new_value); }; controls.throttleMouse = func { if (!getprop("/devices/status/mice/mouse[0]/button[1]")) { return; } var delta = cmdarg().getNode("offset").getValue() * -4; var old_value = getprop("/controls/engines/current-engine/throttle"); var new_value = std.max(0.0, std.min(old_value + delta, 1.0)); setprop("/controls/engines/current-engine/throttle", new_value); }; # 2018.2 introduces new "all" properties for throttle, mixture and prop pitch. # this is the correct way to interface with the axis based controls - use a listener # on the *-all property setlistener("/controls/engines/throttle-all", func{ var value = (1 - getprop("/controls/engines/throttle-all")) / 2; var new_value = std.max(0.0, std.min(value, 1.0)); setprop("/controls/engines/current-engine/throttle", new_value); }, 0, 0); setlistener("/controls/engines/mixture-all", func{ var value = (1 - getprop("/controls/engines/mixture-all")) / 2; var new_value = std.max(0.0, std.min(value, 1.0)); setprop("/controls/engines/current-engine/mixture", new_value); }, 0, 0); # backwards compatibility only - the controls.throttleAxis should not be overridden like this. The joystick binding Throttle (all) has # been replaced and controls.throttleAxis will not be called from the controls binding - so this is to # maintain compatibility with existing joystick xml files. controls.throttleAxis = func { var value = (1 - cmdarg().getNode("setting").getValue()) / 2; var new_value = std.max(0.0, std.min(value, 1.0)); setprop("/controls/engines/current-engine/throttle", new_value); }; controls.adjMixture = func { var delta = arg[0] * controls.THROTTLE_RATE * getprop("/sim/time/delta-realtime-sec"); var old_value = getprop("/controls/engines/current-engine/mixture"); var new_value = std.max(0.0, std.min(old_value + delta, 1.0)); setprop("/controls/engines/current-engine/mixture", new_value); }; # backwards compatibility only - the controls.throttleAxis should not be overridden like this. The joystick binding Throttle (all) has # been replaced and controls.throttleAxis will not be called from the controls binding - so this is to # maintain compatibility with existing joystick xml files. controls.mixtureAxis = func { var value = (1 - cmdarg().getNode("setting").getValue()) / 2; var new_value = std.max(0.0, std.min(value, 1.0)); setprop("/controls/engines/current-engine/mixture", new_value); }; var _axisMode = { 0: controls.perIndexAxisHandler("/controls/engines/current-engine[", "]/throttle"), 1: controls.perIndexAxisHandler("/controls/engines/current-engine[", "]/mixture"), 2: controls.perIndexAxisHandler("/controls/engines/current-engine[", "]/propeller-pitch") }; controls.perEngineSelectedAxisHandler = func(mode) { return _axisMode[mode]; }; controls.stepMagnetos = func { var old_value = getprop("/controls/switches/magnetos"); var new_value = std.max(0, std.min(old_value + arg[0], 3)); setprop("/controls/switches/magnetos", new_value); }; # key 's' calls to this function when it is pressed DOWN even if I overwrite the binding in the -set.xml file! # fun fact: the key UP event can be overwriten! controls.startEngine = func(v = 1) { # Only operate in non-walker mode ('s' is also bound to walk-backward) var view_name = getprop("/sim/current-view/name"); if (view_name == getprop("/sim/view[110]/name") or view_name == getprop("/sim/view[111]/name")) { return; } if (getprop("/engines/active-engine/running")) { setprop("/controls/switches/starter", 0); return; } else { setprop("/controls/switches/magnetos", 3); setprop("/controls/switches/starter", v); } }; var engine_timer = maketimer(UPDATE_PERIOD, func { update(); }); coughing_timer.singleShot = 1; oil_consumption.simulatedTime = 1; setlistener("/sim/signals/fdm-initialized", func { engine_timer.start(); carb_icing_function.start(); coughing_timer.start(); if (oil_consumption.isRunning) { oil_consumption.stop(); } oil_consumption.start(); calculate_real_oiltemp.start(); });