bo105.nas 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987
  1. # (c) Melchior FRANZ < mfranz # flightgear : org >
  2. if (!contains(globals, "cprint"))
  3. var cprint = func nil;
  4. var devel = !!getprop("devel");
  5. var quickstart = !!getprop("quickstart");
  6. var sin = func(a) math.sin(a * D2R);
  7. var cos = func(a) math.cos(a * D2R);
  8. var pow = func(v, w) math.exp(math.ln(v) * w);
  9. var npow = func(v, w) v ? math.exp(math.ln(abs(v)) * w) * (v < 0 ? -1 : 1) : 0;
  10. var clamp = func(v, min = 0, max = 1) v < min ? min : v > max ? max : v;
  11. var normatan = func(x, slope = 1) math.atan2(x, slope) * 2 / math.pi;
  12. var bell = func(x, spread = 2) pow(math.e, -(x * x) / spread);
  13. var max = func(a, b) a > b ? a : b;
  14. var min = func(a, b) a < b ? a : b;
  15. # doors =============================================================
  16. var Doors = {
  17. new: func {
  18. var m = { parents: [Doors] };
  19. m.active = 0;
  20. m.list = [];
  21. foreach (var d; props.globals.getNode("sim/model/bo105/doors").getChildren("door"))
  22. append(m.list, aircraft.door.new(d, 2.5));
  23. return m;
  24. },
  25. next: func {
  26. me.select(me.active + 1);
  27. },
  28. previous: func {
  29. me.select(me.active - 1);
  30. },
  31. select: func(which) {
  32. me.active = which;
  33. if (me.active < 0)
  34. me.active = size(me.list) - 1;
  35. elsif (me.active >= size(me.list))
  36. me.active = 0;
  37. gui.popupTip("Selecting " ~ me.list[me.active].node.getNode("name").getValue());
  38. },
  39. toggle: func {
  40. me.list[me.active].toggle();
  41. },
  42. reset: func {
  43. foreach (var d; me.list)
  44. d.setpos(0);
  45. },
  46. };
  47. # fuel ==============================================================
  48. # density = 6.682 lb/gal [Flight Manual Section 9.2]
  49. # avtur/JET A-1/JP-8
  50. var FUEL_DENSITY = getprop("/consumables/fuel/tank/density-ppg"); # pound per gallon
  51. var GAL2LB = FUEL_DENSITY;
  52. var LB2GAL = 1 / GAL2LB;
  53. var KG2GAL = KG2LB * LB2GAL;
  54. var GAL2KG = 1 / KG2GAL;
  55. var Tank = {
  56. new: func(n) {
  57. var m = { parents: [Tank] };
  58. m.capacity = n.getNode("capacity-gal_us").getValue();
  59. m.level_galN = n.initNode("level-gal_us", m.capacity);
  60. m.level_lbN = n.getNode("level-lbs");
  61. m.consume(0);
  62. return m;
  63. },
  64. level: func {
  65. return me.level_galN.getValue();
  66. },
  67. consume: func(amount) { # US gal (neg. values for feeding)
  68. var level = me.level();
  69. if (amount > level)
  70. amount = level;
  71. level -= amount;
  72. if (level > me.capacity)
  73. level = me.capacity;
  74. me.level_galN.setDoubleValue(level);
  75. me.level_lbN.setDoubleValue(level * GAL2LB);
  76. return amount;
  77. },
  78. };
  79. var fuel = {
  80. init: func {
  81. var fuel = props.globals.getNode("/consumables/fuel");
  82. me.pump_capacity = 6.6 * L2GAL / 60; # same pumps for transfer and supply; from ec135: 6.6 l/min
  83. me.total_galN = fuel.getNode("total-fuel-gals", 1);
  84. me.total_lbN = fuel.getNode("total-fuel-lbs", 1);
  85. me.total_normN = fuel.getNode("total-fuel-norm", 1);
  86. me.main = Tank.new(fuel.getNode("tank[0]"));
  87. me.supply = Tank.new(fuel.getNode("tank[1]"));
  88. var sw = props.globals.getNode("/controls/switches");
  89. setlistener(sw.initNode("fuel/transfer-pump[10]", 1, "BOOL"), func(n) me.trans1 = n.getValue(), 1);
  90. setlistener(sw.initNode("fuel/transfer-pump[1]", 1, "BOOL"), func(n) me.trans2 = n.getValue(), 1);
  91. setlistener("/sim/freeze/fuel", func(n) me.freeze = n.getBoolValue(), 1);
  92. me.capacity = me.main.capacity + me.supply.capacity;
  93. me.warntime = 0;
  94. me.update(0);
  95. },
  96. update: func(dt) {
  97. # transfer pumps (feed supply from main)
  98. var free = me.supply.capacity - me.supply.level();
  99. if (free > 0) {
  100. var trans_flow = (me.trans1 + me.trans2) * me.pump_capacity;
  101. me.supply.consume(-me.main.consume(min(trans_flow * dt, free)));
  102. }
  103. var level = me.main.level() + me.supply.level();
  104. me.total_galN.setDoubleValue(level);
  105. me.total_lbN.setDoubleValue(level * GAL2LB);
  106. me.total_normN.setDoubleValue(level / me.capacity);
  107. },
  108. level: func {
  109. return me.supply.level();
  110. },
  111. consume: func(amount) {
  112. return me.freeze ? 0 : me.supply.consume(amount);
  113. },
  114. };
  115. # engines/rotor =====================================================
  116. var rotor_rpm = props.globals.getNode("rotors/main/rpm");
  117. var torque = props.globals.getNode("rotors/gear/total-torque", 1);
  118. var collective = props.globals.getNode("controls/engines/engine[0]/throttle");
  119. var turbine = props.globals.getNode("sim/model/bo105/turbine-rpm-pct", 1);
  120. var torque_pct = props.globals.getNode("sim/model/bo105/torque-pct", 1);
  121. var target_rel_rpm = props.globals.getNode("controls/rotor/reltarget", 1);
  122. var max_rel_torque = props.globals.getNode("controls/rotor/maxreltorque", 1);
  123. var Engine = {
  124. new: func(n) {
  125. var m = { parents: [Engine] };
  126. m.in = props.globals.getNode("controls/engines", 1).getChild("engine", n, 1);
  127. m.out = props.globals.getNode("engines", 1).getChild("engine", n, 1);
  128. m.airtempN = props.globals.getNode("/environment/temperature-degc");
  129. # input
  130. m.ignitionN = m.in.initNode("ignition", 0, "BOOL");
  131. m.starterN = m.in.initNode("starter", 0, "BOOL");
  132. m.powerN = m.in.initNode("power", 0);
  133. m.magnetoN = m.in.initNode("magnetos", 1, "INT");
  134. # output
  135. m.runningN = m.out.initNode("running", 0, "BOOL");
  136. m.n1_pctN = m.out.initNode("n1-pct", 0);
  137. m.n2_pctN = m.out.initNode("n2-pct", 0);
  138. m.n1N = m.out.initNode("n1-rpm", 0);
  139. m.n2N = m.out.initNode("n2-rpm", 0);
  140. m.totN = m.out.initNode("tot-degc", m.airtempN.getValue());
  141. m.starterLP = aircraft.lowpass.new(3);
  142. m.n1LP = aircraft.lowpass.new(4);
  143. m.n2LP = aircraft.lowpass.new(4);
  144. setlistener("/sim/signals/reinit", func(n) n.getValue() or m.reset(), 1);
  145. m.timer = aircraft.timer.new("/sim/time/hobbs/turbines[" ~ n ~ "]", 10);
  146. m.running = 0;
  147. m.fuelflow = 0;
  148. m.n1 = -1;
  149. m.up = -1;
  150. return m;
  151. },
  152. reset: func {
  153. me.magnetoN.setIntValue(1);
  154. me.ignitionN.setBoolValue(0);
  155. me.starterN.setBoolValue(0);
  156. me.powerN.setDoubleValue(0);
  157. me.runningN.setBoolValue(me.running = 0);
  158. me.starterLP.set(0);
  159. me.n1LP.set(me.n1 = 0);
  160. me.n2LP.set(me.n2 = 0);
  161. },
  162. update: func(dt, trim = 0) {
  163. var starter = me.starterLP.filter(me.starterN.getValue() * 0.19); # starter 15-20% N1max
  164. me.powerN.setValue(me.power = clamp(me.powerN.getValue()));
  165. var power = me.power * 0.97 + trim; # 97% = N2% in flight position
  166. if (me.running)
  167. power += (1 - collective.getValue()) * 0.03; # droop compensator
  168. if (power > 1.12)
  169. power = 1.12; # overspeed restrictor
  170. me.fuelflow = 0;
  171. if (!me.running) {
  172. if (me.n1 > 0.05 and power > 0.05 and me.ignitionN.getValue()) {
  173. me.runningN.setBoolValue(me.running = 1);
  174. me.timer.start();
  175. }
  176. } elsif (power < 0.05 or !fuel.level()) {
  177. me.runningN.setBoolValue(me.running = 0);
  178. me.timer.stop();
  179. } else {
  180. me.fuelflow = power;
  181. }
  182. var lastn1 = me.n1;
  183. me.n1 = me.n1LP.filter(max(me.fuelflow, starter));
  184. me.n2 = me.n2LP.filter(me.n1);
  185. me.up = me.n1 - lastn1;
  186. # temperature
  187. if (me.fuelflow > me.pos.idle)
  188. var target = 440 + (779 - 440) * (0.03 + me.fuelflow - me.pos.idle) / (me.pos.flight - me.pos.idle);
  189. else
  190. var target = 440 * (0.03 + me.fuelflow) / me.pos.idle;
  191. if (me.n1 < 0.4 and me.fuelflow - me.n1 > 0.001) {
  192. target += (me.fuelflow - me.n1) * 7000;
  193. if (target > 980)
  194. target = 980;
  195. }
  196. var airtemp = me.airtempN.getValue();
  197. if (target < airtemp)
  198. target = airtemp;
  199. var decay = (me.up > 0 ? 10 : me.n1 > 0.02 ? 0.01 : 0.001) * dt;
  200. me.totN.setValue((me.totN.getValue() + decay * target) / (1 + decay));
  201. # constant 150 kg/h for now (both turbines)
  202. fuel.consume(75 * KG2GAL * me.fuelflow * dt / 3600);
  203. # derived gauge values
  204. me.n1_pctN.setDoubleValue(me.n1 * 100);
  205. me.n2_pctN.setDoubleValue(me.n2 * 100);
  206. me.n1N.setDoubleValue(me.n1 * 50970);
  207. me.n2N.setDoubleValue(me.n2 * 33290);
  208. },
  209. setpower: func(v) {
  210. var target = (int((me.power + 0.15) * 3) + v) / 3;
  211. var time = abs(me.power - target) * 4;
  212. interpolate(me.powerN, target, time);
  213. },
  214. adjust_power: func(delta, mode = 0) {
  215. if (delta) {
  216. var power = me.powerN.getValue();
  217. if (me.power_min == nil) {
  218. if (delta > 0) {
  219. if (power < me.pos.idle) {
  220. me.power_min = me.pos.cutoff;
  221. me.power_max = me.pos.idle;
  222. } else {
  223. me.power_min = me.pos.idle;
  224. me.power_max = me.pos.flight;
  225. }
  226. } else {
  227. if (power > me.pos.idle) {
  228. me.power_max = me.pos.flight;
  229. me.power_min = me.pos.idle;
  230. } else {
  231. me.power_max = me.pos.idle;
  232. me.power_min = me.pos.cutoff;
  233. }
  234. }
  235. }
  236. me.powerN.setValue(power = clamp(power + delta, me.power_min, me.power_max));
  237. return power;
  238. } elsif (mode) {
  239. me.power_min = me.power_max = nil;
  240. }
  241. },
  242. pos: { cutoff: 0, idle: 0.63, flight: 1 },
  243. };
  244. var engines = {
  245. init: func {
  246. me.engine = [Engine.new(0), Engine.new(1)];
  247. me.trimN = props.globals.initNode("/controls/engines/power-trim");
  248. me.balanceN = props.globals.initNode("/controls/engines/power-balance");
  249. me.commonrpmN = props.globals.initNode("/engines/engine/rpm");
  250. },
  251. reset: func {
  252. me.engine[0].reset();
  253. me.engine[1].reset();
  254. },
  255. update: func(dt) {
  256. # each starter button disables ignition switch of opposite engine
  257. if (me.engine[0].starterN.getValue())
  258. me.engine[1].ignitionN.setBoolValue(0);
  259. if (me.engine[1].starterN.getValue())
  260. me.engine[0].ignitionN.setBoolValue(0);
  261. # update engines
  262. var trim = me.trimN.getValue() * 0.1;
  263. var balance = me.balanceN.getValue() * 0.1;
  264. me.engine[0].update(dt, trim - balance);
  265. me.engine[1].update(dt, trim + balance);
  266. # set rotor
  267. var n2relrpm = max(me.engine[0].n2, me.engine[1].n2);
  268. var n2max = (me.engine[0].n2 + me.engine[1].n2) / 2;
  269. target_rel_rpm.setValue(n2relrpm);
  270. max_rel_torque.setValue(n2max);
  271. me.commonrpmN.setValue(n2max * 33290); # attitude indicator needs pressure
  272. # Warning Box Type K-DW02/01
  273. if (n2max > 0.67) { # 0.63?
  274. setprop("sim/sound/warn2600", n2max > 1.08);
  275. setprop("sim/sound/warn650", abs(me.engine[0].n2 - me.engine[1].n2) > 0.12
  276. or n2max > 0.75 and n2max < 0.95);
  277. } else {
  278. setprop("sim/sound/warn2600", 0);
  279. setprop("sim/sound/warn650", 0);
  280. }
  281. },
  282. adjust_power: func(delta, mode = 0) {
  283. if (!delta) {
  284. engines.engine[0].adjust_power(0, mode);
  285. engines.engine[1].adjust_power(0, mode);
  286. } else {
  287. var p = [0, 0];
  288. for (var i = 0; i < 2; i += 1)
  289. if (controls.engines[i].selected.getValue())
  290. p[i] = engines.engine[i].adjust_power(delta);
  291. gui.popupTip(sprintf("power lever %d%%", 100 * max(p[0], p[1])));
  292. }
  293. },
  294. quickstart: func { # development only
  295. me.engine[0].n1LP.set(1);
  296. me.engine[0].n2LP.set(1);
  297. me.engine[1].n1LP.set(1);
  298. me.engine[1].n2LP.set(1);
  299. procedure.step = 1;
  300. procedure.next();
  301. },
  302. };
  303. var vert_speed_fpm = props.globals.initNode("/velocities/vertical-speed-fpm");
  304. if (devel) {
  305. setprop("/instrumentation/altimeter/setting-inhg", getprop("/environment/pressure-inhg"));
  306. setlistener("/sim/signals/fdm-initialized", func {
  307. settimer(func {
  308. screen.property_display.x = 760;
  309. screen.property_display.y = 200;
  310. screen.property_display.format = "%.3g";
  311. screen.property_display.add(
  312. rotor_rpm,
  313. torque_pct,
  314. target_rel_rpm,
  315. max_rel_torque,
  316. "/controls/engines/power-trim",
  317. "/controls/engines/power-balance",
  318. "/consumables/fuel/total-fuel-gals",
  319. "L",
  320. engines.engine[0].runningN,
  321. engines.engine[0].ignitionN,
  322. "/controls/engines/engine[0]/power",
  323. engines.engine[0].n1_pctN,
  324. engines.engine[0].n2_pctN,
  325. engines.engine[0].totN,
  326. #engines.engine[0].n1N,
  327. #engines.engine[0].n2N,
  328. "R",
  329. engines.engine[1].runningN,
  330. engines.engine[1].ignitionN,
  331. "/controls/engines/engine[1]/power",
  332. engines.engine[1].n1_pctN,
  333. engines.engine[1].n2_pctN,
  334. engines.engine[1].totN,
  335. #engines.engine[1].n1N,
  336. #engines.engine[1].n2N,
  337. "X",
  338. "/sim/model/gross-weight-kg",
  339. "/position/altitude-ft",
  340. "/position/altitude-agl-ft",
  341. "/instrumentation/altimeter/indicated-altitude-ft",
  342. "/environment/temperature-degc",
  343. vert_speed_fpm,
  344. "/velocities/airspeed-kt",
  345. );
  346. }, 1);
  347. });
  348. }
  349. var mouse = {
  350. init: func {
  351. me.x = me.y = nil;
  352. me.savex = nil;
  353. me.savey = nil;
  354. setlistener("/sim/startup/xsize", func(n) me.centerx = int(n.getValue() / 2), 1);
  355. setlistener("/sim/startup/ysize", func(n) me.centery = int(n.getValue() / 2), 1);
  356. setlistener("/devices/status/mice/mouse/mode", func(n) me.mode = n.getValue(), 1);
  357. setlistener("/devices/status/mice/mouse/button[1]", func(n) {
  358. me.mmb = n.getValue();
  359. if (me.mode)
  360. return;
  361. if (me.mmb) {
  362. engines.adjust_power(0, 1);
  363. me.savex = me.x;
  364. me.savey = me.y;
  365. gui.setCursor(me.centerx, me.centery, "none");
  366. } else {
  367. gui.setCursor(me.savex, me.savey, "pointer");
  368. }
  369. }, 1);
  370. setlistener("/devices/status/mice/mouse/x", func(n) me.x = n.getValue(), 1);
  371. setlistener("/devices/status/mice/mouse/y", func(n) me.update(me.y = n.getValue()), 1);
  372. },
  373. update: func {
  374. if (me.mode or !me.mmb)
  375. return;
  376. if (var dy = -me.y + me.centery)
  377. engines.adjust_power(dy * 0.005);
  378. gui.setCursor(me.centerx, me.centery);
  379. },
  380. };
  381. var power = func(v) {
  382. if (controls.engines[0].selected.getValue())
  383. engines.engine[0].setpower(v);
  384. if (controls.engines[1].selected.getValue())
  385. engines.engine[1].setpower(v);
  386. }
  387. var startup = func {
  388. if (procedure.stage < 0) {
  389. procedure.step = 1;
  390. procedure.next();
  391. }
  392. }
  393. var shutdown = func {
  394. if (procedure.stage > 0) {
  395. procedure.step = -1;
  396. procedure.next();
  397. }
  398. }
  399. var procedure = {
  400. stage: -999,
  401. step: nil,
  402. loopid: 0,
  403. reset: func {
  404. me.loopid += 1;
  405. me.stage = -999;
  406. step = nil;
  407. engines.reset();
  408. },
  409. next: func(delay = 0) {
  410. if (crashed)
  411. return;
  412. if (me.stage < 0 and me.step > 0 or me.stage > 0 and me.step < 0)
  413. me.stage = 0;
  414. settimer(func { me.stage += me.step; me.process(me.loopid) }, delay * !quickstart);
  415. },
  416. process: func(id) {
  417. id == me.loopid or return;
  418. # startup
  419. if (me.stage == 1) {
  420. cprint("", "1: press start button #1 -> spool up turbine #1 to N1 8.6--15%");
  421. setprop("/controls/rotor/brake", 0);
  422. engines.engine[0].ignitionN.setValue(1);
  423. engines.engine[0].starterN.setValue(1);
  424. me.next(4);
  425. } elsif (me.stage == 2) {
  426. cprint("", "2: move power lever #1 forward -> fuel injection");
  427. engines.engine[0].powerN.setValue(0.13);
  428. me.next(2.5);
  429. } elsif (me.stage == 3) {
  430. cprint("", "3: turbine #1 ignition (wait for EGT stabilization)");
  431. me.next(4.5);
  432. } elsif (me.stage == 4) {
  433. cprint("", "4: move power lever #1 to idle position -> engine #1 spools up to N1 63%");
  434. engines.engine[0].powerN.setValue(0.63);
  435. me.next(5);
  436. } elsif (me.stage == 5) {
  437. cprint("", "5: release start button #1\n");
  438. engines.engine[0].starterN.setValue(0);
  439. engines.engine[0].ignitionN.setValue(0);
  440. me.next(3);
  441. } elsif (me.stage == 6) {
  442. cprint("", "6: press start button #2 -> spool up turbine #2 to N1 8.6--15%");
  443. engines.engine[1].ignitionN.setValue(1);
  444. engines.engine[1].starterN.setValue(1);
  445. me.next(5);
  446. } elsif (me.stage == 7) {
  447. cprint("", "7: move power lever #2 forward -> fuel injection");
  448. engines.engine[1].powerN.setValue(0.13);
  449. me.next(2);
  450. } elsif (me.stage == 8) {
  451. cprint("", "8: turbine #2 ignition (wait for EGT stabilization)");
  452. me.next(5);
  453. } elsif (me.stage == 9) {
  454. cprint("", "9: move power lever #2 to idle position -> engine #2 spools up to N1 63%");
  455. engines.engine[1].powerN.setValue(0.63);
  456. me.next(8);
  457. } elsif (me.stage == 10) {
  458. cprint("", "10: release start button #2\n");
  459. engines.engine[1].starterN.setValue(0);
  460. engines.engine[1].ignitionN.setValue(0);
  461. me.next(1);
  462. } elsif (me.stage == 11) {
  463. cprint("", "11: move both power levers forward -> turbines spool up to 100%");
  464. engines.engine[0].powerN.setValue(1);
  465. engines.engine[1].powerN.setValue(1);
  466. # shutdown
  467. } elsif (me.stage == -1) {
  468. cprint("", "-1: power lever in idle position; cool engines");
  469. engines.engine[0].starterN.setValue(0);
  470. engines.engine[1].starterN.setValue(0);
  471. engines.engine[0].ignitionN.setValue(0);
  472. engines.engine[1].ignitionN.setValue(0);
  473. engines.engine[0].powerN.setValue(0.63);
  474. engines.engine[1].powerN.setValue(0.63);
  475. me.next(40);
  476. } elsif (me.stage == -2) {
  477. cprint("", "-2: engines shut down");
  478. engines.engine[0].powerN.setValue(0);
  479. engines.engine[1].powerN.setValue(0);
  480. me.next(40);
  481. } elsif (me.stage == -3) {
  482. cprint("", "-3: rotor brake\n");
  483. setprop("/controls/rotor/brake", 1);
  484. }
  485. },
  486. };
  487. # torquemeter
  488. var torque_val = 0;
  489. torque.setDoubleValue(0);
  490. var update_torque = func(dt) {
  491. var f = dt / (0.2 + dt);
  492. torque_val = torque.getValue() * f + torque_val * (1 - f);
  493. torque_pct.setDoubleValue(torque_val / 5300);
  494. }
  495. # blade vibration absorber pendulum
  496. var pendulum = props.globals.getNode("/sim/model/bo105/absorber-angle-deg", 1);
  497. var update_absorber = func {
  498. pendulum.setDoubleValue(90 * clamp(abs(rotor_rpm.getValue()) / 90));
  499. }
  500. var vibration = { # and noise ...
  501. init: func {
  502. me.lonN = props.globals.initNode("/rotors/main/vibration/longitudinal");
  503. me.latN = props.globals.initNode("/rotors/main/vibration/lateral");
  504. me.soundN = props.globals.initNode("/sim/sound/vibration");
  505. me.airspeedN = props.globals.getNode("/velocities/airspeed-kt");
  506. me.vertspeedN = props.globals.getNode("/velocities/vertical-speed-fps");
  507. me.groundspeedN = props.globals.getNode("/velocities/groundspeed-kt");
  508. me.speeddownN = props.globals.getNode("/velocities/speed-down-fps");
  509. me.angleN = props.globals.initNode("/velocities/descent-angle-deg");
  510. me.dir = 0;
  511. },
  512. update: func(dt) {
  513. var airspeed = me.airspeedN.getValue();
  514. # Reduce VNE by 3 kt for every 1000 ft of altitude for the AS350
  515. var VNE = 155 - 0.003 * getprop("/instrumentation/altimeter/pressure-alt-ft");
  516. if (airspeed > VNE) { # overspeed vibration
  517. var frequency = 2000 + 500 * rand();
  518. var v = 0.49 + 0.5 * normatan(airspeed - 160, 10);
  519. var intensity = v;
  520. var noise = v * internal;
  521. } elsif (airspeed > 30) { # Blade Vortex Interaction (BVI) 8 deg, 65 kts max?
  522. var frequency = rotor_rpm.getValue() * 4 * 60;
  523. var down = me.speeddownN.getValue() * FT2M;
  524. var level = me.groundspeedN.getValue() * NM2M / 3600;
  525. me.angleN.setDoubleValue(var angle = math.atan2(down, level) * R2D);
  526. var speed = math.sqrt(level * level + down * down) * MPS2KT;
  527. angle = bell(angle - 9, 13);
  528. speed = bell(speed - 65, 450);
  529. var v = angle * speed;
  530. var intensity = v * 0.10;
  531. var noise = v * (1 - internal * 0.4);
  532. } else { # hover
  533. var rpm = rotor_rpm.getValue();
  534. var frequency = rpm * 4 * 60;
  535. var coll = bell(collective.getValue(), 0.5);
  536. var ias = bell(airspeed, 600);
  537. var vert = bell(me.vertspeedN.getValue() * 0.5, 400);
  538. var rpm = 0.477 + 0.5 * normatan(rpm - 350, 30) * 1.025;
  539. var v = coll * ias * vert * rpm;
  540. var intensity = v * 0.10;
  541. var noise = v * (1 - internal * 0.4);
  542. }
  543. me.dir += dt * frequency;
  544. me.lonN.setValue(cos(me.dir) * intensity);
  545. me.latN.setValue(sin(me.dir) * intensity);
  546. me.soundN.setValue(noise);
  547. },
  548. };
  549. # sound =============================================================
  550. # stall sound
  551. var stall = props.globals.getNode("rotors/main/stall", 1);
  552. var stall_filtered = props.globals.getNode("rotors/main/stall-filtered", 1);
  553. var stall_val = 0;
  554. stall.setDoubleValue(0);
  555. var update_stall = func(dt) {
  556. var s = stall.getValue();
  557. if (s < stall_val) {
  558. var f = dt / (0.3 + dt);
  559. stall_val = s * f + stall_val * (1 - f);
  560. } else {
  561. stall_val = s;
  562. }
  563. var c = collective.getValue();
  564. stall_filtered.setDoubleValue(stall_val + 0.006 * (1 - c));
  565. }
  566. # skid slide sound
  567. var Skid = {
  568. new: func(n) {
  569. var m = { parents: [Skid] };
  570. var soundN = props.globals.getNode("sim/model/bo105/sound", 1).getChild("slide", n, 1);
  571. var gearN = props.globals.getNode("gear", 1).getChild("gear", n, 1);
  572. m.compressionN = gearN.getNode("compression-norm", 1);
  573. m.rollspeedN = gearN.getNode("rollspeed-ms", 1);
  574. m.frictionN = gearN.getNode("ground-friction-factor", 1);
  575. m.wowN = gearN.getNode("wow", 1);
  576. m.volumeN = soundN.getNode("volume", 1);
  577. m.pitchN = soundN.getNode("pitch", 1);
  578. m.compressionN.setDoubleValue(0);
  579. m.rollspeedN.setDoubleValue(0);
  580. m.frictionN.setDoubleValue(0);
  581. m.volumeN.setDoubleValue(0);
  582. m.pitchN.setDoubleValue(0);
  583. m.wowN.setBoolValue(1);
  584. m.self = n;
  585. return m;
  586. },
  587. update: func {
  588. me.wow = me.wowN.getValue();
  589. if (me.wow < 0.5)
  590. return me.volumeN.setDoubleValue(0);
  591. var rollspeed = abs(me.rollspeedN.getValue());
  592. me.pitchN.setDoubleValue(rollspeed * 0.6);
  593. var s = normatan(20 * rollspeed);
  594. var f = clamp((me.frictionN.getValue() - 0.5) * 2);
  595. var c = clamp(me.compressionN.getValue() * 2);
  596. var vol = s * f * c;
  597. me.volumeN.setDoubleValue(vol > 0.1 ? vol : 0);
  598. },
  599. };
  600. var skids = [];
  601. for (var i = 0; i < 4; i += 1)
  602. append(skids, Skid.new(i));
  603. var update_slide = func {
  604. foreach (var s; skids)
  605. s.update();
  606. }
  607. var internal = 1;
  608. setlistener("sim/current-view/view-number", func {
  609. internal = getprop("sim/current-view/internal");
  610. }, 1);
  611. var volume = props.globals.getNode("sim/model/bo105/sound/volume", 1);
  612. var update_volume = func {
  613. var door_open = 0;
  614. foreach (var d; doors.list) {
  615. if (!d.enabledN.getValue() or d.positionN.getValue() > 0.001) {
  616. door_open = 1;
  617. break;
  618. }
  619. }
  620. volume.setDoubleValue(1 - (0.8 - 0.6 * door_open) * internal);
  621. }
  622. # crash handler =====================================================
  623. var crash = func {
  624. if (arg[0]) {
  625. # crash
  626. setprop("sim/model/bo105/tail-angle-deg", 35);
  627. setprop("sim/model/bo105/shadow", 0);
  628. setprop("sim/model/bo105/doors/door[0]/position-norm", 0.2);
  629. setprop("sim/model/bo105/doors/door[1]/position-norm", 0.9);
  630. setprop("sim/model/bo105/doors/door[2]/position-norm", 0.2);
  631. setprop("sim/model/bo105/doors/door[3]/position-norm", 0.6);
  632. setprop("sim/model/bo105/doors/door[4]/position-norm", 0.1);
  633. setprop("sim/model/bo105/doors/door[5]/position-norm", 0.05);
  634. setprop("rotors/main/rpm", 0);
  635. setprop("rotors/main/blade[0]/flap-deg", -60);
  636. setprop("rotors/main/blade[1]/flap-deg", -50);
  637. setprop("rotors/main/blade[2]/flap-deg", -40);
  638. setprop("rotors/main/blade[3]/flap-deg", -30);
  639. setprop("rotors/main/blade[0]/incidence-deg", -30);
  640. setprop("rotors/main/blade[1]/incidence-deg", -20);
  641. setprop("rotors/main/blade[2]/incidence-deg", -50);
  642. setprop("rotors/main/blade[3]/incidence-deg", -55);
  643. setprop("rotors/tail/rpm", 0);
  644. strobe_switch.setValue(0);
  645. beacon_switch.setValue(0);
  646. nav_light_switch.setValue(0);
  647. engines.engine[0].n2_pctN.setValue(0);
  648. engines.engine[1].n2_pctN.setValue(0);
  649. torque_pct.setValue(torque_val = 0);
  650. stall_filtered.setValue(stall_val = 0);
  651. } else {
  652. # uncrash (for replay)
  653. setprop("sim/model/bo105/tail-angle-deg", 0);
  654. setprop("sim/model/bo105/shadow", 1);
  655. doors.reset();
  656. setprop("rotors/tail/rpm", 2219);
  657. setprop("rotors/main/rpm", 442);
  658. for (i = 0; i < 4; i += 1) {
  659. setprop("rotors/main/blade[" ~ i ~ "]/flap-deg", 0);
  660. setprop("rotors/main/blade[" ~ i ~ "]/incidence-deg", 0);
  661. }
  662. strobe_switch.setValue(1);
  663. beacon_switch.setValue(1);
  664. engines.engine[0].n2_pct.setValue(100);
  665. engines.engine[1].n2_pct.setValue(100);
  666. }
  667. }
  668. # "manual" rotor animation for flight data recorder replay ============
  669. var rotor_step = props.globals.getNode("sim/model/bo105/rotor-step-deg");
  670. var blade1_pos = props.globals.getNode("rotors/main/blade[0]/position-deg", 1);
  671. var blade2_pos = props.globals.getNode("rotors/main/blade[1]/position-deg", 1);
  672. var blade3_pos = props.globals.getNode("rotors/main/blade[2]/position-deg", 1);
  673. var blade4_pos = props.globals.getNode("rotors/main/blade[3]/position-deg", 1);
  674. var rotorangle = 0;
  675. var rotoranim_loop = func {
  676. var i = rotor_step.getValue();
  677. if (i >= 0.0) {
  678. blade1_pos.setValue(rotorangle);
  679. blade2_pos.setValue(rotorangle + 90);
  680. blade3_pos.setValue(rotorangle + 180);
  681. blade4_pos.setValue(rotorangle + 270);
  682. rotorangle += i;
  683. settimer(rotoranim_loop, 0.1);
  684. }
  685. }
  686. var init_rotoranim = func {
  687. if (rotor_step.getValue() >= 0.0)
  688. settimer(rotoranim_loop, 0.1);
  689. }
  690. # view management ===================================================
  691. var elapsedN = props.globals.getNode("/sim/time/elapsed-sec", 1);
  692. var flap_mode = 0;
  693. var down_time = 0;
  694. controls.flapsDown = func(v) {
  695. if (!flap_mode) {
  696. if (v < 0) {
  697. down_time = elapsedN.getValue();
  698. flap_mode = 1;
  699. dynamic_view.lookat(
  700. 5, # heading left
  701. -20, # pitch up
  702. 0, # roll right
  703. 0.2, # right
  704. 0.6, # up
  705. 0.85, # back
  706. 0.2, # time
  707. 55, # field of view
  708. );
  709. } elsif (v > 0) {
  710. flap_mode = 2;
  711. gui.popupTip("AUTOTRIM", 1e10);
  712. aircraft.autotrim.start();
  713. }
  714. } else {
  715. if (flap_mode == 1) {
  716. if (elapsedN.getValue() < down_time + 0.2)
  717. return;
  718. dynamic_view.resume();
  719. } elsif (flap_mode == 2) {
  720. aircraft.autotrim.stop();
  721. gui.popdown();
  722. }
  723. flap_mode = 0;
  724. }
  725. }
  726. # register function that may set me.heading_offset, me.pitch_offset, me.roll_offset,
  727. # me.x_offset, me.y_offset, me.z_offset, and me.fov_offset
  728. #
  729. dynamic_view.register(func {
  730. var lowspeed = 1 - normatan(me.speedN.getValue() / 50);
  731. var r = sin(me.roll) * cos(me.pitch);
  732. me.heading_offset = # heading change due to
  733. (me.roll < 0 ? -50 : -30) * r * abs(r); # roll left/right
  734. me.pitch_offset = # pitch change due to
  735. (me.pitch < 0 ? -50 : -50) * sin(me.pitch) * lowspeed # pitch down/up
  736. + 15 * sin(me.roll) * sin(me.roll); # roll
  737. me.roll_offset = # roll change due to
  738. -15 * r * lowspeed; # roll
  739. });
  740. var adjust_fov = func {
  741. var w = getprop("/sim/startup/xsize");
  742. var h = getprop("/sim/startup/ysize");
  743. var ar = clamp(max(w, h) / min(w, h), 0, 2);
  744. var fov = 60 + (ar - (4 / 3)) * 10 / (16 / 9 - 4 / 3);
  745. setprop("/sim/view/config/default-field-of-view-deg", fov);
  746. if (internal)
  747. setprop("/sim/current-view/config/default-field-of-view-deg", fov);
  748. }
  749. setlistener("/sim/startup/xsize", adjust_fov);
  750. setlistener("/sim/startup/ysize", adjust_fov, 1);
  751. # main() ============================================================
  752. var delta_time = props.globals.getNode("/sim/time/delta-sec", 1);
  753. var hi_heading = props.globals.getNode("/instrumentation/heading-indicator/indicated-heading-deg", 1);
  754. var vertspeed = props.globals.initNode("/velocities/vertical-speed-fps");
  755. var gross_weight_lb = props.globals.initNode("/yasim/gross-weight-lbs");
  756. var gross_weight_kg = props.globals.initNode("/sim/model/gross-weight-kg");
  757. props.globals.getNode("/instrumentation/adf/rotation-deg", 1).alias(hi_heading);
  758. var main_loop = func {
  759. props.globals.removeChild("autopilot");
  760. if (replay)
  761. setprop("/position/gear-agl-m", getprop("/position/altitude-agl-ft") * 0.3 - 1.2);
  762. vert_speed_fpm.setDoubleValue(vertspeed.getValue() * 60);
  763. #gross_weight_kg.setDoubleValue(gross_weight_lb.getValue() * LB2KG);
  764. var dt = delta_time.getValue();
  765. update_torque(dt);
  766. update_stall(dt);
  767. update_slide();
  768. update_volume();
  769. update_absorber();
  770. fuel.update(dt);
  771. engines.update(dt);
  772. vibration.update(dt);
  773. settimer(main_loop, 0);
  774. }
  775. var replay = 0;
  776. var crashed = 0;
  777. var doors = Doors.new();
  778. var first_init = 1;
  779. setlistener("/sim/signals/fdm-initialized", func {
  780. if (!first_init) return;
  781. first_init = 0;
  782. gui.menuEnable("autopilot", 1);
  783. init_rotoranim();
  784. vibration.init();
  785. engines.init();
  786. fuel.init();
  787. mouse.init();
  788. collective.setDoubleValue(1);
  789. setlistener("/sim/signals/reinit", func(n) {
  790. n.getBoolValue() and return;
  791. cprint("32;1", "reinit");
  792. procedure.reset();
  793. collective.setDoubleValue(1);
  794. #aircraft.livery.rescan();
  795. reconfigure();
  796. if (crashed)
  797. crash(crashed = 0);
  798. });
  799. setlistener("sim/crashed", func(n) {
  800. cprint("31;1", "crashed ", n.getValue());
  801. engines.engine[0].timer.stop();
  802. engines.engine[1].timer.stop();
  803. if (n.getBoolValue())
  804. crash(crashed = 1);
  805. });
  806. setlistener("/sim/freeze/replay-state", func(n) {
  807. replay = n.getValue();
  808. cprint("33;1", replay ? "replay" : "pause");
  809. if (crashed)
  810. crash(!n.getBoolValue())
  811. });
  812. main_loop();
  813. if (devel and quickstart)
  814. engines.quickstart();
  815. });