Optica_engine.txt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. # Edgley Optica
  2. #
  3. # Engine management routines
  4. #
  5. # Gary Neely aka 'Buckaroo'
  6. #
  7. var UPDATE_INTERVAL = 0.2; # Update engines every 0.2 secs.
  8. var MAX_RPM = 2200; # Above this the engine starts to accumulate strain
  9. # from over-rev issues if MAX_RPM_T exceeded
  10. var MAX_RPM_CLK = 60; # Secs; Time up to this value doesn't count toward strain
  11. var MAX_IDLE = 800; # Above this the engine starts to accumulate strain
  12. # from oil viscosity issues
  13. var MAX_STRAIN = 20000; # Arbitrary strain value, when exceeded, engine dies
  14. var OILV_DELTA_WARMUP = 0.002; # Viscosity increment per cycle when warming up
  15. var OILV_DELTA_COOLDN = 0.0002; # Viscosity decrement per cycle when cooling down
  16. var VISC_PENALTY = 40; # Maximum reduction of oil pressure for viscosity
  17. var COLD_OIL_FACTOR = 25; # Arbitrary strain value for running engine over idle when cold
  18. # This is killer, so keep value small or users will whine
  19. var MAX_OK_OILT = 80; # Maximum best oil temperature
  20. var MIN_OK_OILT = 60; # Minimum best oil temperature
  21. var MIN_OILP = 70; # Minimum acceptable oil pressure
  22. var MAX_OILP = 150; # Highest value oil pressure will register
  23. var MAX_OK_OILP = 90; # Maximum best oil pressure
  24. var MIN_OK_OILP = 70; # Minimum best oil pressure
  25. var MIN_OILP = 50; # Minimum acceptable oil pressure, currently not used
  26. var TARGET_FUEL_P = 5; # Target best fuel pressure
  27. var CYLT_MAX = 270; # Engine always takes damage above this temp
  28. var TARGET_CYLT_HIGH = 230; # Target best cylinder head temp, high end
  29. # Engine takes damage above this temp after a time interval
  30. var TARGET_CYLT_LOW = 150; # Target best cylinder head temp, low end
  31. var CYLT_MIN = 100; # Low end of operating cylinder head temp
  32. var MAX_CYLT_STRAIN_CLK = 60; # Time in secs that must pass before engine under CYLT_MAX
  33. # but over CYLT_HIGH starts taking damage
  34. var DELTA_T_F = 0.1; # Change in temperature master factor,
  35. # inc to make changes faster, dec to slow temp changes
  36. var DELTA_T_ENV = 0.014; # Change in temp due to ambient air factor
  37. var DELTA_T_THRUST = 0.006; # Change in temp due to power factor
  38. var DELTA_T_AS = 0.02; # Change in temp due to airspeed-induced air factor
  39. var MIXTURE_CUTOFF = 0.8; # Mixture setting: below this value, no cooling benefits
  40. var MIXTURE_FACTOR = 14; # Mixture cooling benefits multiplier
  41. var OPOH = 50; # Engine operational overhead value
  42. var PROP_AIR = 0.01; # Factor for prop air moving over engine
  43. var AMBIENT_REDUCTION = 0.25; # Factor for reduction of ambient cooling under max operating range
  44. var MIXTURE_PEAK = 0.74; # Observed mixture setting for peak power
  45. var MIXTURE_BIAS = 10; # Max degrees C to augment cyl temp at mixture peak
  46. var ENGINE_MASS = 1; # Engine mass factor; not currently used, but the idea is to
  47. # inc this to slow temperature changes
  48. var eng_checks = props.globals.getNode("/sim/Optica/engine-checks");
  49. var eng_warns = props.globals.getNode("/sim/Optica/engine-warns");
  50. var tanks = props.globals.getNode("/consumables/fuel").getChildren("tank");
  51. var engine = props.globals.getNode("/engines/engine[0]");
  52. var controls = props.globals.getNode("controls/engines/engine[0]");
  53. var airspeed = props.globals.getNode("/velocities/airspeed-kt");
  54. var envtemp = props.globals.getNode("/environment/temperature-degc");
  55. # Defined under Optica-fuel.nas:
  56. #var fuel_scalar = props.globals.getNode("consumables/fuel/consumption-scalar").getValue();
  57. var oilt_target = (MAX_OK_OILT-MIN_OK_OILT)/2 + MIN_OK_OILT; # Calculate a nice mid-range temp value to seek
  58. var oilp_target = (MAX_OK_OILP-MIN_OK_OILP)/2 + MIN_OK_OILP; # Calculate a nice mid-range pressure value to seek
  59. # Functions for enabling engine options:
  60. var eng_checks_set = func {
  61. if (eng_checks.getValue()) { eng_checks.setValue(0); var mssg = "Engine parameter checks disabled."; }
  62. else { eng_checks.setValue(1); var mssg = "Engine parameter checks enabled."; }
  63. Optica_screenmssg.fg = [1, 1, 1, 1];
  64. Optica_screenmssg.write(mssg);
  65. }
  66. var eng_warns_set = func {
  67. if (eng_warns.getValue()) { eng_warns.setValue(0); var mssg = "Engine parameter warnings disabled."; }
  68. else { eng_warns.setValue(1); var mssg = "Engine parameter warnings enabled."; }
  69. Optica_screenmssg.fg = [1, 1, 1, 1];
  70. Optica_screenmssg.write(mssg);
  71. }
  72. # Engine warning messages:
  73. var eng_checks_overrev = func(i) {
  74. i+=1;
  75. var mssg = "Engine "~i~" over-rev warning.";
  76. Optica_screenmssg.fg = [1, 0.5, 0, 1];
  77. Optica_screenmssg.write(mssg);
  78. }
  79. var eng_checks_overheat = func(i) {
  80. i+=1;
  81. var mssg = "Engine "~i~" over-heating warning.";
  82. Optica_screenmssg.fg = [1, 0.5, 0, 1];
  83. Optica_screenmssg.write(mssg);
  84. }
  85. var eng_checks_oilcold = func(i) {
  86. i+=1;
  87. var mssg = "Engine "~i~" oil temperature warning.";
  88. Optica_screenmssg.fg = [1, 0.5, 0, 1];
  89. Optica_screenmssg.write(mssg);
  90. }
  91. var eng_checks_damage = func(i,y) {
  92. i+=1;
  93. var mssg = "Engine "~i~" has passed 25% damage.";
  94. if (y > MAX_STRAIN * 0.9) { mssg = "Engine "~i~" is critical."; }
  95. elsif (y > MAX_STRAIN * 0.75) { mssg = "Engine "~i~" has passed 75% damage."; }
  96. elsif (y > MAX_STRAIN * 0.5) { mssg = "Engine "~i~" has passed 50% damage."; }
  97. Optica_screenmssg2.fg = [1, 0.5, 0, 1];
  98. Optica_screenmssg2.write(mssg);
  99. }
  100. var eng_checks_failure = func(i) {
  101. i+=1;
  102. var mssg = "Engine "~i~" failure.";
  103. Optica_screenmssg2.fg = [1, 0, 0, 1];
  104. Optica_screenmssg2.write(mssg);
  105. }
  106. var FtoC = func (f) { # Fahrenheit to Celsius
  107. return (f-32) * 5 / 9;
  108. }
  109. # Primary engine loop:
  110. var engine_update = func {
  111. # Adjust fuel flow:
  112. engine.getNode("fuel-flow-adj").setValue(engine.getNode("fuel-flow-gph").getValue()*fuel_scalar);
  113. if (engine.getNode("running").getValue() == 1) { # Is engine running?
  114. var strain = engine.getNode("strain").getValue();
  115. if (strain > MAX_STRAIN) { # Don't let strain value get absurd when engine
  116. strain = MAX_STRAIN; # failure is disabled
  117. engine.getNode("strain").setValue(strain);
  118. }
  119. # Calculate engine strain due to over-rev
  120. var strain_clk_rev = engine.getNode("strain-clk-rev").getValue(); # Get time we've been operating over-revved
  121. if (engine.getNode("rpm").getValue() > MAX_RPM) {
  122. if (strain_clk_rev < MAX_RPM_CLK) { # Update time allowed for over-rev
  123. interpolate(engine.getNode("strain-clk-rev"),MAX_RPM_CLK,MAX_RPM_CLK-strain_clk_rev);
  124. }
  125. # If time allowed over RPM is exceeded:
  126. if (strain_clk_rev >= MAX_RPM_CLK) { # Add more strain to engine
  127. if (eng_checks.getValue()) {
  128. var strain_add = engine.getNode("rpm").getValue() - MAX_RPM;
  129. engine.getNode("strain").setValue(strain + strain_add);
  130. }
  131. if (eng_warns.getValue()) { eng_checks_overrev(i); } # Notice to user
  132. }
  133. }
  134. else {
  135. # Under max RPM, so allow timer to fall back to 0
  136. if (strain_clk_rev > 0) {
  137. interpolate(engine.getNode("strain-clk-rev"),0,strain_clk_rev);
  138. }
  139. }
  140. # Fuel pressure
  141. if (engine.getNode("fuel-press").getValue() < TARGET_FUEL_P) {
  142. interpolate(engine.getNode("fuel-press"), TARGET_FUEL_P, 3);
  143. }
  144. # Simplified oil temp, viscosity, pressure
  145. # Note that viscosity is a dimensionless normalized value,
  146. # not a true viscosity number
  147. var oilv = engine.getNode("oil-visc").getValue();
  148. if (oilv > 0) {
  149. oilv = oilv - OILV_DELTA_WARMUP; # Determine new oilv as necessary
  150. engine.getNode("oil-visc").setValue(oilv); # Save viscosity
  151. var oilp = ((MAX_OILP - oilp_target) * oilv) + oilp_target;# Viscosity determines position between max oilp and target best oilp
  152. engine.getNode("oil-press").setValue(oilp); # Save oilp, probably not necessary to interpolate
  153. # Oilt rises from ambient as viscosity falls
  154. var oilt = (oilt_target - envtemp.getValue()) * (1-oilv) + envtemp.getValue();
  155. engine.getNode("oiltempc").setValue(oilt); # Save oilt as deg C
  156. # Oil pressure check:
  157. if (oilp > MAX_OK_OILP and engine.getNode("rpm").getValue() > MAX_IDLE) {
  158. if (eng_checks.getValue()) {
  159. engine.getNode("strain").setValue(engine.getNode("strain").getValue() + (COLD_OIL_FACTOR/oilv));
  160. }
  161. if (eng_warns.getValue()) { eng_checks_oilcold(i); } # Notice to user
  162. }
  163. }
  164. # Cyl head temp change calculations:
  165. # Currently this accounts for environmental cooling,
  166. # airflow cooling, and thrust/rpm heating. It should
  167. # also account for cooling due to rich mixture, but
  168. # doesn't yet.
  169. var cyl_temp = engine.getNode("cyltempc").getValue();
  170. var thrust = engine.getNode("prop-thrust").getValue();
  171. var mixture = controls[i].getNode("mixture").getValue();
  172. var throttle = controls[i].getNode("throttle").getValue();
  173. var dT_env = (envtemp.getValue() - cyl_temp) * DELTA_T_ENV;
  174. var dT_thrust = (thrust + OPOH) * DELTA_T_THRUST;
  175. var dT_airspeed = airspeed.getValue() * DELTA_T_AS;
  176. var dT_mixture = mixture - MIXTURE_CUTOFF;
  177. if (dT_mixture < 0) { dT_mixture = 0; } # DeltaT due to rich mixture benefits
  178. else { dT_mixture = dT_mixture * MIXTURE_FACTOR; }
  179. if (cyl_temp <= TARGET_CYLT_LOW) { # When below best operating low temp:
  180. dT_env = dT_env * AMBIENT_REDUCTION; # DeltaT due to ambient reduction benefits
  181. dT_airspeed = 0; # No airspeed benefits below best operating temp low range
  182. # Linearly reduce mixture benefits down to ambient temp
  183. dT_mixture = 0; # No significant mixture benefits in this range
  184. }
  185. elsif (cyl_temp < TARGET_CYLT_HIGH) { # When withing best operating temp range:
  186. # Normalize temperature range:
  187. var temp_norm = (cyl_temp - TARGET_CYLT_LOW) / (TARGET_CYLT_HIGH - TARGET_CYLT_LOW);
  188. # Linearly reduce ambient benefits from full to AMBIENT_REDUCTION over range
  189. dT_env = dT_env * (AMBIENT_REDUCTION + (AMBIENT_REDUCTION * temp_norm));
  190. # Linearly reduce airspeed benefits from full to none
  191. dT_airspeed = dT_airspeed * temp_norm;
  192. # Linearly reduce mixture benefits from full to none
  193. if (dT_mixture > 0) { dT_mixture = dT_mixture * temp_norm; }
  194. }
  195. var delta_cyl_temp = (dT_env + dT_thrust - dT_airspeed - dT_mixture) * DELTA_T_F;
  196. cyl_temp = cyl_temp + delta_cyl_temp;
  197. engine.getNode("cyltempc").setValue(cyl_temp);
  198. # Calculate engine strain due to over-temp
  199. var strain_clk_temp = engine.getNode("strain-clk-temp").getValue(); # Get time that we've been operating over-temp
  200. if (cyl_temp > CYLT_MAX) { # Above this temp engine damage always occurs
  201. if (eng_checks.getValue()) {
  202. var strain_add = cyl_temp - TARGET_CYLT_HIGH;
  203. engine.getNode("strain").setValue(strain + strain_add);
  204. }
  205. if (eng_warns.getValue()) { eng_checks_overheat(i); } # Notice to user
  206. }
  207. if (cyl_temp > TARGET_CYLT_HIGH) { # Above this temp engine damage occurs after a period of time
  208. if (strain_clk_temp < MAX_CYLT_STRAIN_CLK) { # Update time allowed for over-temp
  209. interpolate(engine.getNode("strain-clk-temp"),MAX_CYLT_STRAIN_CLK,MAX_CYLT_STRAIN_CLK-strain_clk_temp);
  210. }
  211. # If time allowed over temp is exceeded:
  212. if (strain_clk_temp >= MAX_CYLT_STRAIN_CLK) { # Add more strain to engine
  213. if (eng_checks.getValue()) {
  214. var strain_add = cyl_temp - TARGET_CYLT_HIGH;
  215. engine.getNode("strain").setValue(strain + strain_add);
  216. }
  217. if (eng_warns.getValue()) { eng_checks_overheat(i); } # Notice to user
  218. }
  219. }
  220. else {
  221. # Under temp limits, so allow timer to fall back to 0
  222. if (strain_clk_temp > 0) {
  223. interpolate(engine.getNode("strain-clk-temp"),0,strain_clk_temp);
  224. }
  225. }
  226. # Mixture adjustments change thrust values by about 5% which
  227. # which is hard to notice on the gauge. So this is a fudged
  228. # addition to make the temperature peak more when the mixture
  229. # is leaned out to highest power output. This means the gauge
  230. # will read a little high, but failure tests will still be
  231. # based on true cyl temp.
  232. var bias = 0;
  233. if (mixture >= MIXTURE_PEAK) {
  234. bias = ((1 - mixture) / (1 - MIXTURE_PEAK)) * MIXTURE_BIAS * throttle;
  235. }
  236. else {
  237. bias = (mixture / MIXTURE_PEAK) * MIXTURE_BIAS * throttle;
  238. }
  239. engine.getNode("cyltempc-biased").setValue(cyl_temp+bias);
  240. # For testing and debugging:
  241. #engine.getNode("cyl-dt").setValue(delta_cyl_temp);
  242. #engine.getNode("cyl-dte").setValue(dT_env);
  243. #engine.getNode("cyl-dtt").setValue(dT_thrust);
  244. #engine.getNode("cyl-dta").setValue(dT_airspeed);
  245. #engine.getNode("cyl-dtm").setValue(dT_mixture);
  246. # Engine damage warnings
  247. if (eng_warns.getValue() and strain > MAX_STRAIN * 0.25) {
  248. eng_checks_damage(i,strain);
  249. }
  250. if (eng_checks.getValue()) {
  251. # Kill engine if over-strained
  252. strain = engine.getNode("strain").getValue();
  253. if (strain >= MAX_STRAIN) {
  254. engine.getNode("overrev").setValue(1);
  255. engine.getNode("running").setValue(0);
  256. engine.getNode("out-of-fuel").setValue(1);
  257. tanks[i].getNode("selected").setValue(0);
  258. interpolate(engine.getNode("fuel-press"), 0, 5);
  259. if (eng_warns.getValue()) { eng_checks_failure(i); }
  260. }
  261. }
  262. }
  263. else { # Engine not running
  264. # Kill fuel pressure
  265. if (engine.getNode("fuel-press").getValue() > 0) {
  266. interpolate(engine.getNode("fuel-press"), 0, 10);
  267. }
  268. # Cyl temp cool-down:
  269. var cyl_temp = engine.getNode("cyltempc").getValue();
  270. var dT_env = (envtemp.getValue() - cyl_temp) * DELTA_T_ENV;
  271. var dT_airspeed = airspeed.getValue() * DELTA_T_AS;
  272. if (cyl_temp <= TARGET_CYLT_LOW) { # When below best operating low temp:
  273. dT_env = dT_env * AMBIENT_REDUCTION; # Reduce ambient cooling
  274. dT_airspeed = 0; # No airspeed benefits below best operating temp low range
  275. }
  276. elsif (cyl_temp < TARGET_CYLT_HIGH) { # When withing best operating temp range:
  277. # Normalize range:
  278. var temp_norm = (cyl_temp - TARGET_CYLT_LOW) / (TARGET_CYLT_HIGH - TARGET_CYLT_LOW);
  279. # Linearly reduce ambient benefits from full to AMBIENT_REDUCTION over range
  280. dT_env = dT_env * (AMBIENT_REDUCTION + (AMBIENT_REDUCTION * temp_norm));
  281. dT_airspeed = dT_airspeed * temp_norm; # Linearly reduce airspeed benefits from full to none
  282. }
  283. var delta_cyl_temp = (dT_env - dT_airspeed) * DELTA_T_F;
  284. cyl_temp = cyl_temp + delta_cyl_temp;
  285. engine.getNode("cyltempc").setValue(cyl_temp);
  286. engine.getNode("cyltempc-biased").setValue(cyl_temp);
  287. # Oil cool-down; see main oil section for notes
  288. var oilv = engine.getNode("oil-visc").getValue();
  289. if (oilv < 1) {
  290. var oilt = (oilt_target - envtemp.getValue()) * (1-oilv) + envtemp.getValue();
  291. engine.getNode("oiltempc").setValue(oilt);
  292. var oilp = ((MAX_OILP - oilp_target) * oilv) + oilp_target;
  293. engine.getNode("oil-press").setValue(oilp);
  294. engine.getNode("oil-visc").setValue(oilv + OILV_DELTA_COOLDN);
  295. }
  296. } # end engines-not-running
  297. settimer(engine_loop, UPDATE_INTERVAL);
  298. }
  299. var engine_setup = func {
  300. # Set engines to ambient temp on startup
  301. engines[0].getNode("cyltempc").setValue(envtemp.getValue());
  302. engines[0].getNode("oiltempc").setValue(envtemp.getValue());
  303. # Uncomment to pre-warm engines for testing:
  304. #engines[0].getNode("cyltempc").setValue((TARGET_CYLT_HIGH-TARGET_CYLT_LOW)/2+TARGET_CYLT_LOW);
  305. engine_update();
  306. }
  307. var EngineInit = func {
  308. settimer(engine_setup, 1); # Give a few seconds for environment vars to initialize
  309. }