helicopter-fcs.nas 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917
  1. #
  2. # Flight Control System for Helicopters by Tatsuhiro Nishioka
  3. # $Id$
  4. #
  5. var enableDebug = func() {
  6. setprop("/controls/flight/fcs/switches/debug", 1);
  7. }
  8. var debugEnabled = func() {
  9. var debugStatus = getprop("/controls/flight/fcs/switches/debug");
  10. if (debugStatus == 1) {
  11. return 1;
  12. } else {
  13. return 0;
  14. }
  15. }
  16. var dumpParameters = func() {
  17. debug.dump(props.globals.getNode("/controls/flight/fcs/gains").getValues());
  18. }
  19. #
  20. # FCSFilter - base class for FCS components like CAS and SAS
  21. #
  22. var FCSFilter = {
  23. #
  24. # new - constructor
  25. # input_path: a property path for a filter input
  26. # nil is equivalent to "/controls/flight/"
  27. # output_path: a property path for a filter output
  28. #
  29. new : func(input_path, output_path) {
  30. var obj = { parents : [FCSFilter],
  31. input_path : input_path,
  32. output_path : output_path };
  33. obj.axis_conv = {'roll' : 'aileron', 'pitch' : 'elevator', 'yaw' : 'rudder' };
  34. obj.body_conv = {'roll' : 'v', 'pitch' : 'u' };
  35. obj.last_body_fps = {'roll' : 0.0, 'pitch' : 0.0 };
  36. obj.last_pos = {'roll' : 0.0, 'pitch' : 0.0, 'yaw' : 0.0};
  37. return obj;
  38. },
  39. #
  40. # updateSensitivities: read sensitivitiy values for all axis from the property
  41. #
  42. updateSensitivities : func() {
  43. me.sensitivities = props.globals.getNode("/controls/flight/fcs/gains/sensitivities").getValues();
  44. },
  45. #
  46. # read - gets input command for a given axis from input_path
  47. #
  48. read : func(axis) {
  49. if (me.input_path == nil or me.input_path == "") {
  50. return getprop("/controls/flight/" ~ me.axis_conv[axis]);
  51. } else {
  52. var value = getprop(me.input_path ~ "/" ~ axis);
  53. value = int(value * 1000) / 1000.0;
  54. }
  55. },
  56. #
  57. # write - outputs command for a given axis into output_path
  58. # this will be the output of an next command filter (like SAS)
  59. #
  60. write : func(axis, value) {
  61. if (me.output_path == nil or me.output_path == '') {
  62. setprop("/controls/flight/fcs/" ~ axis, me.limit(value, 1.0));
  63. } else {
  64. setprop(me.output_path ~ "/" ~ axis, me.limit(value, 1.0));
  65. }
  66. },
  67. #
  68. # toggleFilterStatus - toggles engage/disengage FCS function
  69. # name: FCS filter name; one of /controls/flight/fcs/switches/*
  70. #
  71. toggleFilterStatus : func(name) {
  72. var messages = ["disengaged", "engaged"];
  73. var path = "/controls/flight/fcs/switches/" ~ name;
  74. var status = getprop(path);
  75. setprop(path, 1 - status);
  76. screen.log.write(name ~ " " ~ messages[1 - status]);
  77. },
  78. #
  79. # getStatus - returns 1 if a given function is engaged
  80. # name: FCS filter name; one of /controls/flight/fcs/switches/*
  81. #
  82. getStatus : func(name) {
  83. var path = "/controls/flight/fcs/switches/" ~ name;
  84. return getprop(path);
  85. },
  86. #
  87. # limit - cut out a given value between +range to -range
  88. # value: number to be adjusted
  89. # range: absolute number for specifying the range
  90. limit : func(value, range) {
  91. if (value > range) {
  92. return range;
  93. } elsif (value < -range) {
  94. return - range;
  95. }
  96. return value;
  97. },
  98. max : func(val1, val2) {
  99. return (val1 > val2) ? val1 : val2;
  100. },
  101. min : func(val1, val2) {
  102. return (val1 > val2) ? val2 : val1;
  103. },
  104. #
  105. # calcCounterBodyFPS - calculates counter-force command to kill movement in each axis
  106. # axis: one of 'roll', 'pitch', or 'yaw'
  107. # input: input (0.0 - 1.0) for a given axis
  108. # offset_deg:
  109. #
  110. calcCounterBodyFPS : func(axis, input, offset_deg) {
  111. var position = getprop("/orientation/" ~ axis ~ "-deg");
  112. var body_fps = 0;
  113. var last_body_fps = me.last_body_fps[axis];
  114. var reaction_gain = 0;
  115. var heading = getprop("/orientation/heading-deg");
  116. var wind_speed_fps = getprop("/environment/wind-speed-kt") * 1.6878099;
  117. var wind_direction = getprop("/environment/wind-from-heading-deg");
  118. var wind_direction -= heading;
  119. var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
  120. var gear_pos = getprop("/gear/gear[0]/compression-norm") + getprop("/gear/gear[1]/compression-norm");
  121. var counter_fps = 0;
  122. var fps_axis = me.body_conv[axis]; # convert from {roll, pitch} to {u, v}
  123. var target_pos = offset_deg;
  124. var brake_deg = 0;
  125. body_fps = getprop("/velocities/" ~ fps_axis ~ "Body-fps");
  126. if (axis == 'roll') {
  127. var wind_fps = math.sin(wind_direction / 180 * math.pi) * wind_speed_fps;
  128. } else {
  129. var wind_fps = math.cos(wind_direction / 180 * math.pi) * wind_speed_fps;
  130. }
  131. var brake_freq = getprop("/controls/flight/fcs/gains/afcs/fps-" ~ axis ~ "-brake-freq");
  132. var brake_gain = getprop("/controls/flight/fcs/gains/afcs/fps-brake-gain-" ~ axis);
  133. body_fps -= wind_fps;
  134. var dfps = body_fps - me.last_body_fps[axis];
  135. var fps_coeff = getprop("/controls/flight/fcs/gains/afcs/fps-" ~ axis ~ "-coeff");
  136. target_pos -= int(body_fps * 100) / 100 * fps_coeff;
  137. if (axis == 'roll' and gear_pos > 0.0 and position > 0) {
  138. target_pos -= position * gear_pos / 5;
  139. }
  140. reaction_gain = getprop("/controls/flight/fcs/gains/afcs/fps-reaction-gain-" ~ axis);
  141. var brake_sensitivity = (axis == 'roll') ? 1 : 1;
  142. if (math.abs(position + rate / brake_freq * brake_sensitivity) > math.abs(target_pos)) {
  143. if (math.abs(dfps) > 1) {
  144. dfps = 1;
  145. }
  146. var error_deg = target_pos - position;
  147. brake_deg = (error_deg - rate / brake_freq) * math.abs(dfps * 10) * brake_gain;
  148. if (target_pos > 0) {
  149. brake_deg = me.min(brake_deg, 0);
  150. } else {
  151. brake_deg = me.max(brake_deg, 0);
  152. }
  153. }
  154. counter_fps = me.limit((target_pos + brake_deg) * reaction_gain, 1.0);
  155. if (debugEnabled() == 1) {
  156. setprop("/controls/flight/fcs/afcs/status/ah-" ~ fps_axis ~ "body-fps", body_fps);
  157. setprop("/controls/flight/fcs/afcs/status/ah-" ~ fps_axis ~ "body-wind-fps", wind_fps);
  158. setprop("/controls/flight/fcs/afcs/status/ah-" ~ axis ~ "-target-deg", target_pos);
  159. setprop("/controls/flight/fcs/afcs/status/ah-" ~ axis ~ "-rate", rate);
  160. setprop("/controls/flight/fcs/afcs/status/ah-delta-" ~ fps_axis ~ "body-fps", dfps);
  161. setprop("/controls/flight/fcs/afcs/status/ah-" ~ axis ~ "-brake-deg", brake_deg);
  162. setprop("/controls/flight/fcs/afcs/status/counter-fps-" ~ axis, counter_fps);
  163. }
  164. me.last_pos[axis] = position;
  165. me.last_body_fps[axis] = body_fps;
  166. return me.limit(counter_fps + input * 0.2, 1.0);
  167. },
  168. };
  169. #
  170. # AFCS - Automatic Flight Control System
  171. #
  172. var AFCS = {
  173. new : func(input_path, output_path) {
  174. var obj = FCSFilter.new(input_path, output_path);
  175. obj.parents = [FCSFilter, AFCS];
  176. return obj;
  177. },
  178. #
  179. # toggle* - I/F methods for Instruments
  180. #
  181. toggleAutoHover : func() {
  182. me.toggleFilterStatus("auto-hover");
  183. },
  184. toggleAirSpeedLock : func() {
  185. me.toggleFilterStatus("air-speed-lock");
  186. },
  187. toggleHeadingLock : func() {
  188. me.toggleFilterStatus("heading-lock");
  189. },
  190. toggleAltitudeLock : func() {
  191. me.toggleFilterStatus("altitude-lock");
  192. },
  193. #
  194. # auto hover - locks vBody_fps and uBody_fps regardless of wind speed/direction
  195. #
  196. autoHover : func(axis, input) {
  197. if (axis == 'yaw') {
  198. return input;
  199. } else {
  200. var offset_deg = getprop("/controls/flight/fcs/gains/afcs/fps-" ~ axis ~ "-offset-deg");
  201. return me.calcCounterBodyFPS(axis, input, offset_deg);
  202. }
  203. },
  204. altitudeLock : func(axis, input) {
  205. # not implemented yet
  206. return input;
  207. },
  208. headingLock : func(axis, input) {
  209. # not implementet yet
  210. return input;
  211. },
  212. #
  213. # applying all AFCS functions
  214. # only auto hover is available at this moment
  215. #
  216. apply : func(axis) {
  217. var input = me.read(axis);
  218. var hover_status = me.getStatus("auto-hover");
  219. if (hover_status == 0) {
  220. me.write(axis, input);
  221. return;
  222. }
  223. me.write(axis, me.autoHover(axis, input));
  224. }
  225. };
  226. #
  227. # SAS : Stability Augmentation System - a rate damper
  228. #
  229. var SAS = {
  230. #
  231. # new
  232. # input_path: is a base path to input axis; nil for using raw input from KB/JS
  233. # output_path: is a base path to output axis; nis for using /controls/flight/fcs
  234. # with input_path / output_path, you can connect SAS, CAS, and more control filters
  235. #
  236. new : func(input_path, output_path) {
  237. var obj = FCSFilter.new(input_path, output_path);
  238. obj.parents = [FCSFilter, SAS];
  239. return obj;
  240. },
  241. toggleEnable : func() {
  242. me.toggleFilterStatus("sas");
  243. },
  244. #
  245. # calcGain - get gain for each axis based on air speed and dynamic pressure
  246. # axis: one of 'roll', 'pitch', or 'yaw'
  247. #
  248. calcGain : func(axis) {
  249. var mach = getprop("/velocities/mach");
  250. var initial_gain = getprop("/controls/flight/fcs/gains/sas/" ~ axis);
  251. var gain = initial_gain - 0.1 * mach * mach;
  252. if (math.abs(gain) < math.abs(initial_gain) * 0.01 or gain * initial_gain < 0) {
  253. gain = initial_gain * 0.01;
  254. }
  255. return gain;
  256. },
  257. #
  258. # calcAuthorityLimit - returns SAS authority limit using a given limit and mach number
  259. #
  260. calcAuthorityLimit : func() {
  261. var mach = getprop("/velocities/mach");
  262. var min_mach = 0.038;
  263. me.authority_limit = getprop("/controls/flight/fcs/gains/sas/authority-limit");
  264. var limit = me.authority_limit;
  265. if (math.abs(mach < min_mach)) {
  266. limit += (min_mach - math.abs(mach)) / min_mach * (1 - me.authority_limit) * 0.95;
  267. }
  268. if (debugEnabled() == 1) {
  269. setprop("/controls/flight/fcs/sas/status/authority-limit", limit);
  270. }
  271. return limit;
  272. },
  273. #
  274. # apply - apply SAS damper to a given input axis
  275. # axis: one of 'roll', 'pitch', or 'yaw'
  276. #
  277. apply : func(axis) {
  278. me.updateSensitivities();
  279. var status = me.getStatus("sas");
  280. var input = me.read(axis);
  281. if (status == 0) {
  282. me.write(axis, input);
  283. return;
  284. }
  285. var mach = getprop("/velocities/mach");
  286. var value = 0;
  287. var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
  288. var gain = me.calcGain(axis);
  289. var limit = me.calcAuthorityLimit();
  290. if (math.abs(rate) >= me.sensitivities[axis]) {
  291. value = - gain * rate;
  292. if (value > limit) {
  293. value = limit;
  294. } elsif (value < - limit) {
  295. value = - limit;
  296. }
  297. }
  298. me.write(axis, value + input);
  299. }
  300. };
  301. #
  302. # CAS : Control Augmentation System - makes your aircraft more meneuverable
  303. #
  304. var CAS = {
  305. new : func(input_path, output_path) {
  306. var obj = FCSFilter.new(input_path, output_path);
  307. obj.parents = [FCSFilter, CAS];
  308. setprop("/autopilot/locks/altitude", '');
  309. setprop("/autopilot/locks/heading", '');
  310. obj.setCASControlThresholds();
  311. return obj;
  312. },
  313. calcRollRateAdjustment : func {
  314. var position = getprop("/orientation/roll-deg");
  315. return math.abs(math.sin(position / 180 * math.pi)) / 6;
  316. },
  317. #
  318. # calcHeadingAdjustment - returns roll axis output for stabilizing heading
  319. #
  320. calcHeadingAdjustment : func {
  321. if (getprop("/controls/flight/fcs/switches/heading-adjuster") == 1) {
  322. var gain = getprop("/controls/flight/fcs/gains/cas/output/heading-adjuster-gain");
  323. var yaw_rate = getprop("/orientation/yaw-rate-degps");
  324. var limit = getprop("/controls/flight/fcs/gains/cas/output/heading-adjuster-limit");
  325. var adjuster = yaw_rate * gain;
  326. return me.limit(adjuster, limit);
  327. } else {
  328. return 0;
  329. }
  330. },
  331. #
  332. # calcSideSlipAdjustment - returns yaw axis output for preventing side slip
  333. #
  334. calcSideSlipAdjustment : func {
  335. if (getprop("/controls/flight/fcs/switches/sideslip-adjuster") == 0) {
  336. return 0;
  337. }
  338. var mach = getprop("/velocities/mach");
  339. var slip = -getprop("/orientation/side-slip-deg"); # inverted after a change in side-slip sign (bug #901)
  340. var min_speed_threshold = getprop("/controls/flight/fcs/gains/cas/input/anti-side-slip-min-speed");
  341. if (mach < min_speed_threshold) { # works only if air speed > min_speed_threshold
  342. slip = 0;
  343. }
  344. var anti_slip_gain = getprop("/controls/flight/fcs/gains/cas/output/anti-side-slip-gain");
  345. var roll_deg = getprop("/orientation/roll-deg");
  346. var gain_adjuster = me.min(math.abs(mach) / 0.060, 1) * me.limit(0.2 + math.sqrt(math.abs(roll_deg)/10), 3);
  347. anti_slip_gain *= gain_adjuster;
  348. if (debugEnabled() == 1) {
  349. setprop("/controls/flight/fcs/cas/status/anti-side-slip", slip * anti_slip_gain);
  350. }
  351. return slip * anti_slip_gain;
  352. },
  353. #
  354. # isInverted - returns 1 if aircraft is inverted (roll > 90 or roll < -90)
  355. #
  356. isInverted : func() {
  357. var roll_deg = getprop("/orientation/roll-deg");
  358. if (roll_deg > 90 or roll_deg < -90)
  359. return 1;
  360. else
  361. return 0;
  362. },
  363. # FIXME: command for CAS is just a temporal one
  364. #
  365. # calcCommand - returns CAS output for each axis
  366. #
  367. calcCommand: func (axis, input) {
  368. var output = 0;
  369. var mach = getprop("/velocities/mach");
  370. var input_gain = me.calcGain(axis);
  371. var output_gain = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis);
  372. var target_rate = input * input_gain;
  373. var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
  374. var drate = target_rate - rate;
  375. if (axis == 'pitch' and me.isInverted() == 1) {
  376. drate = - drate;
  377. }
  378. var attitudeControlThreshold = getprop("/controls/flight/fcs/gains/cas/input/attitude-control-threshold");
  379. var rateControlThreshold = getprop("/controls/flight/fcs/gains/cas/input/rate-control-threshold");
  380. var locks = {'pitch' : getprop("/autopilot/locks/altitude"),
  381. 'roll' : getprop("/autopilot/locks/heading")};
  382. setprop("/controls/flight/fcs/cas/target_" ~ axis ~ "rate", target_rate);
  383. setprop("/controls/flight/fcs/cas/delta_" ~ axis, drate);
  384. if (axis == 'roll' or axis == 'pitch') {
  385. if (math.abs(input) > rateControlThreshold) {
  386. return input;
  387. } elsif (math.abs(input) > attitudeControlThreshold or locks[axis] != '') {
  388. output = drate * output_gain;
  389. } else {
  390. output = me.calcAttitudeCommand(axis);
  391. }
  392. if (axis == 'roll' and math.abs(mach) < 0.035) {
  393. # FIXME: I don't know if OH-1 has this one
  394. output += me.calcCounterBodyFPS(axis, input, -0.8);
  395. }
  396. } elsif (axis == 'yaw') {
  397. if (getprop("/controls/flight/fcs/switches/tail-rotor-adjuster") == 0) {
  398. output = input;
  399. } else {
  400. output = drate * output_gain + me.calcSideSlipAdjustment();
  401. }
  402. } else {
  403. output = drate * output_gain;
  404. }
  405. return output;
  406. },
  407. toggleEnable : func() {
  408. me.toggleFilterStatus("cas");
  409. },
  410. #
  411. # toggle enable / disable attitude control
  412. # you can make similar function that changes parameters
  413. # in attitude-control-limit and rate-control-limit
  414. # at controls/flight/fcs/gains/cas/input
  415. # CAS changes its behavior when roll/pitch axis inputs reaches each limit.
  416. # e.g. when attitude-control-limit is 0.7 and rate-control-limit is 0.9,
  417. # giving 0.6 for roll holds bank angle, 0.8 keeps roll rate,
  418. # and 1.0 makes roll at maximum roll rate.
  419. # Sets of initial values for these limits are stored at
  420. # controls/fcs/gains/cas/{attitude,rate}
  421. #
  422. toggleAttitudeControl : func() {
  423. me.toggleFilterStatus("attitude-control");
  424. me.setCASControlThresholds();
  425. },
  426. setCASControlThresholds : func()
  427. {
  428. if (me.getStatus("attitude-control") == 1) {
  429. var params = props.globals.getNode("controls/flight/fcs/gains/cas/control/attitude").getValues();
  430. props.globals.getNode("controls/flight/fcs/gains/cas/input").setValues(params);
  431. } else {
  432. var params = props.globals.getNode("controls/flight/fcs/gains/cas/control/rate").getValues();
  433. props.globals.getNode("controls/flight/fcs/gains/cas/input").setValues(params);
  434. }
  435. },
  436. #
  437. # calcAttitudeCommand - Attitude base Augmentation output for roll and pitch axis
  438. # axis: either 'roll' or 'pitch'
  439. #
  440. calcAttitudeCommand : func(axis) {
  441. var input_gain = getprop("/controls/flight/fcs/gains/cas/input/attitude-" ~ axis);
  442. var output_gain = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis);
  443. var brake_freq = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis ~ "-brake-freq");
  444. var brake_gain = getprop("/controls/flight/fcs/gains/cas/output/" ~ axis ~ "-brake");
  445. var trim = getprop("/controls/flight/" ~ me.axis_conv[axis] ~ "-trim");
  446. var current_deg = getprop("/orientation/" ~ axis ~ "-deg");
  447. var rate = getprop("/orientation/" ~ axis ~ "-rate-degps");
  448. var target_deg = (me.read(axis) + trim) * input_gain;
  449. if (axis == 'roll' and math.abs(target_deg) < 0.1) {
  450. # rolls a bit to counteract the heading changes only if target roll rate = 0
  451. target_deg += me.calcHeadingAdjustment();
  452. }
  453. var command_deg = 0;
  454. if (target_deg != 0) {
  455. command_deg = (0.094 * math.ln(math.abs(target_deg)) + 0.53) * target_deg;
  456. }
  457. var error_deg = command_deg - current_deg;
  458. if (axis == 'pitch' and me.isInverted() == 1) {
  459. error_deg = - error_deg;
  460. }
  461. var brake_deg = (error_deg - rate / brake_freq) * math.abs(error_deg) * brake_gain;
  462. if (command_deg > 0) {
  463. brake_deg = me.min(brake_deg, 0);
  464. } else {
  465. brake_deg = me.max(brake_deg, 0);
  466. }
  467. if (debugEnabled() == 1) {
  468. var monitor_prefix = me.output_path ~ "/status/" ~ axis;
  469. setprop(monitor_prefix ~ "-target_deg", target_deg);
  470. setprop(monitor_prefix ~ "-error_deg", error_deg);
  471. setprop(monitor_prefix ~ "-brake_deg", brake_deg);
  472. setprop(monitor_prefix ~ "-deg", current_deg);
  473. setprop(monitor_prefix ~ "-rate", -rate);
  474. }
  475. return (error_deg + brake_deg) * output_gain;
  476. },
  477. #
  478. # calcGain - returns gain for a given axis using a given gain and speed
  479. # FixMe: gain should be calculated using both speed and dynamic pressure
  480. #
  481. calcGain : func(axis) {
  482. var mach = getprop("/velocities/mach");
  483. var input_gain = getprop("/controls/flight/fcs/gains/cas/input/" ~ axis);
  484. var gain = input_gain;
  485. if (axis == 'pitch') {
  486. gain += 0.1 * mach * mach;
  487. } elsif (axis== 'yaw') {
  488. gain *= ((1 - mach) * (1 - mach));
  489. }
  490. if (gain * input_gain < 0.0 ) {
  491. gain = 0;
  492. }
  493. if (debugEnabled() == 1) {
  494. setprop("/controls/flight/fcs/cas/gain-" ~ axis, gain);
  495. }
  496. return gain;
  497. },
  498. #
  499. # apply - public method that outputs CAS command for a given axis to output_path
  500. # input is read from input_path
  501. # axis: one of 'roll', 'pitch', or 'yaw'
  502. #
  503. apply : func(axis) {
  504. me.updateSensitivities();
  505. var input = me.read(axis);
  506. var status = me.getStatus("cas");
  507. var cas_command = 0;
  508. # FIXME : hmm, a bit nasty. CAS should be enabled even with auto-hover....
  509. if (status == 0 or (me.getStatus("auto-hover") == 1 and axis != 'yaw')) {
  510. me.write(axis, input);
  511. return;
  512. }
  513. cas_command = me.calcCommand(axis, input);
  514. me.write(axis, cas_command);
  515. }
  516. };
  517. #
  518. # Tail hstab, "stabilator," for stabilize the nose
  519. #
  520. var Stabilator = {
  521. new : func() {
  522. var obj = { parents : [Stabilator] };
  523. me.gainTable = props.globals.getNode("/controls/flight/fcs/gains/stabilator").getChildren('gain-table');
  524. return obj;
  525. },
  526. toggleEnable : func {
  527. var status = getprop("/controls/flight/fcs/switches/auto-stabilator");
  528. getprop("/controls/flight/fcs/switches/auto-stabilator", 1 - status);
  529. },
  530. #
  531. # calcPosition - returns stabilator position (output) depending on
  532. # predefined gain table and mach number
  533. #
  534. calcPosition : func() {
  535. var speed = getprop("/velocities/mach") / 0.001497219; # in knot
  536. var index = int(math.abs(speed) / 10);
  537. if (index >= size(me.gainTable) - 1) {
  538. index = size(me.gainTable) - 2;
  539. }
  540. var gain = me.gainTable[index].getValue();
  541. var gainAmb = me.gainTable[index-1].getValue();
  542. var mod = math.mod(int(math.abs(speed)), 10);
  543. var position = gain * ((10 - mod) / 10) + gainAmb * mod / 10;
  544. if (speed < -20) {
  545. position = - position;
  546. }
  547. return position;
  548. },
  549. #
  550. # apply - public method for Stabilator control
  551. # no axis is required since it is only for hstab
  552. #
  553. apply : func() {
  554. var status = getprop("/controls/flight/fcs/switches/auto-stabilator");
  555. if (status == 0) {
  556. return;
  557. }
  558. var gain = getprop("/controls/flight/fcs/gains/stabilator/stabilator-gain");
  559. var mach = getprop("/velocities/mach");
  560. var throttle = getprop("/controls/flight/throttle");
  561. var stabilator_norm = 0;
  562. stabilator_norm = me.calcPosition();
  563. setprop("/controls/flight/fcs/stabilator", stabilator_norm);
  564. }
  565. };
  566. #
  567. # Automatic tail rotor adjuster depending on collective/throttle status
  568. #
  569. var TailRotorCollective = {
  570. new : func() {
  571. var obj = FCSFilter.new("/controls/engines/engine[1]", "/controls/flight/fcs/tail-rotor");
  572. obj.parents = [FCSFilter, TailRotorCollective];
  573. obj.adjuster = 0.0;
  574. return obj;
  575. },
  576. #
  577. # apply - public method for tail rotor adjuster
  578. # no axis is required
  579. #
  580. apply : func() {
  581. var throttle = me.read("throttle");
  582. var pedal_pos_deg = getprop("/controls/flight/fcs/yaw");
  583. var cas_input = cas.read('yaw');
  584. var cas_input_gain = cas.calcGain('yaw');
  585. var target_rate = cas_input * cas_input_gain;
  586. var rate = getprop("/orientation/yaw-rate-degps");
  587. var error_rate = getprop("/controls/flight/fcs/cas/delta_yaw");
  588. var error_adjuster_gain = getprop("/controls/flight/fcs/gains/tail-rotor/error-adjuster-gain");
  589. var minimum = getprop("/controls/flight/fcs/gains/tail-rotor/src-minimum");
  590. var maximum = getprop("/controls/flight/fcs/gains/tail-rotor/src-maximum");
  591. var low_limit = getprop("/controls/flight/fcs/gains/tail-rotor/low-limit");
  592. var high_limit = getprop("/controls/flight/fcs/gains/tail-rotor/high-limit");
  593. var authority_limit = getprop("/controls/flight/fcs/gains/tail-rotor/authority-limit");
  594. var output = 0;
  595. var range = maximum - minimum;
  596. if (throttle < minimum) {
  597. output = low_limit;
  598. } elsif (throttle > maximum) {
  599. output = high_limit;
  600. } else {
  601. output = low_limit + (throttle - minimum) / range * (high_limit - low_limit);
  602. }
  603. # CAS driven tail rotor thrust adjuster
  604. if(error_rate == nil) {
  605. error_rate = 1;
  606. }
  607. if(error_adjuster_gain == nil) {
  608. error_adjuster_gain = 1;
  609. }
  610. me.adjuster = error_rate * error_adjuster_gain;
  611. me.adjuster = me.limit(me.adjuster, authority_limit);
  612. output += me.adjuster;
  613. setprop("/controls/flight/fcs/tail-rotor/error-rate", error_rate);
  614. setprop("/controls/flight/fcs/tail-rotor/adjuster", me.adjuster);
  615. me.write("throttle", output);
  616. }
  617. };
  618. # Back-up FCS
  619. # It automatically disable CAS and shifts to
  620. # the backup mode (e.g. SAS only or direct link mode)
  621. #
  622. var BackupFCS = {
  623. new : func() {
  624. var obj = { parents : [BackupFCS] };
  625. obj.switches = {'cas' : 0, 'sas' : 1, 'attitude-control' : 0 }; # default backup switches
  626. obj.normalSwitches = props.globals.getNode("/controls/flight/fcs/switches").getValues();
  627. setprop("/controls/flight/fcs/failures/manual-backup-mode", 0);
  628. setprop("/controls/flight/fcs/switches/backup-mode", 0);
  629. return obj;
  630. },
  631. # checkFCSFailures - detects FCS failures
  632. # returns 1 if failure (or manual backup mode) is detected, 0 otherwise
  633. #
  634. checkFCSFailures : func()
  635. {
  636. # not fully implemented yet
  637. if (getprop("/controls/flight/fcs/failures/manual-backup-mode") == 1) {
  638. return 1;
  639. } else {
  640. return 0;
  641. }
  642. },
  643. #
  644. # shiftToBackupMode - overwrites switches for force entering backup mode
  645. #
  646. shiftToBackupMode : func() {
  647. if (me.switches != nil) {
  648. var switchNode = props.globals.getNode("/controls/flight/fcs/switches");
  649. switchNode.setValues(me.switches);
  650. setprop("/controls/flight/fcs/switches/backup-mode", 1);
  651. }
  652. },
  653. #
  654. # shiftToNormalMode - bring switches back to normal mode
  655. # switches for normalMode are captured at BackupFCS.new
  656. shiftToNormalMode : func() {
  657. if (me.normalSwitches != nil) {
  658. props.globals.getNode("/controls/flight/fcs/switches").setValues(me.normalSwitches);
  659. setprop("/controls/flight/fcs/switches/backup-mode", 0);
  660. }
  661. },
  662. #
  663. # setBackupMode - specifies set of values on FCS switches
  664. # switches: hash of FCS switch values that will be set to
  665. # controls/flight/fcs/switches on backup mode
  666. # only values to be overwritten must be specified
  667. # e.g. {'cas' : 0, 'sas' : 1}
  668. #
  669. setBackupMode : func(switches) {
  670. me.switches = switches
  671. },
  672. #
  673. # update - main I/F for BackupFCS
  674. #
  675. update : func() {
  676. if (me.checkFCSFailures() == 1) {
  677. me.shiftToBackupMode();
  678. } elsif (getprop("/controls/flight/fcs/switches/backup-mode") == 1) {
  679. me.shiftToNormalMode();
  680. }
  681. },
  682. #
  683. # toggleBackupMode - I/F for Cockpit Panel
  684. #
  685. toggleBackupMode : func() {
  686. var mode = getprop("/controls/flight/fcs/failures/manual-backup-mode");
  687. setprop("/controls/flight/fcs/failures/manual-backup-mode", 1 - mode);
  688. }
  689. };
  690. var sas = nil;
  691. var cas = nil;
  692. var afcs = nil;
  693. var stabilator = nil;
  694. var tail = nil;
  695. var backup = nil;
  696. var count = 0;
  697. #
  698. # AFCS main loop
  699. # This runs at 60Hz (on every other update of /rotors/main/cone-deg)
  700. #
  701. var update = func {
  702. count += 1;
  703. # AFCS, CAS, and SAS run at 60Hz
  704. rpm = getprop("/rotors/main/rpm");
  705. # AFCS, CAS, and SAS run at 60Hz only when engine rpm >= 10
  706. # this rpm filter prevents CAS/SAS work when engine is not running,
  707. # which may cause Nasal runtime error
  708. if (math.mod(count, 2) == 0 or rpm < 10) {
  709. return;
  710. }
  711. cas.apply('roll');
  712. cas.apply('pitch');
  713. cas.apply('yaw');
  714. afcs.apply('roll');
  715. afcs.apply('pitch');
  716. afcs.apply('yaw');
  717. sas.apply('roll');
  718. sas.apply('pitch');
  719. sas.apply('yaw');
  720. stabilator.apply();
  721. tail.apply();
  722. backup.update();
  723. }
  724. # Factory default configuration values
  725. # DO NOT CHANGE THESE VALUES.!
  726. # You can change some of these values in per-aircraft nasal file.
  727. # See Aircraft/OH-1/Nasal/OH1.nas for more detail
  728. #
  729. var default_fcs_params = {
  730. 'gains' : {
  731. 'afcs' : {
  732. # Auto Hover parameters
  733. 'fps-brake-gain-pitch' : 1.8,
  734. 'fps-brake-gain-roll' : 0.8,
  735. 'fps-pitch-brake-freq' : 3,
  736. 'fps-pitch-coeff' : -0.95,
  737. 'fps-pitch-offset-deg' : 0.9,
  738. 'fps-reaction-gain-pitch' : -0.8,
  739. 'fps-reaction-gain-roll' : 0.3436,
  740. 'fps-roll-brake-freq' : 8,
  741. 'fps-roll-coeff' : 0.8,
  742. 'fps-roll-offset-deg' : -0.8
  743. },
  744. 'cas' : {
  745. 'input' : { # Input gains for CAS
  746. 'roll' : 30,
  747. 'pitch' : -60,
  748. 'yaw' : 30,
  749. 'attitude-roll' : 80,
  750. 'attitude-pitch' : -80,
  751. 'attitude-control-threshold' : 0.0, # input threshold that CAS changes attitude-base control to rate-base control
  752. 'rate-control-threshold' : 0.95, # input threshold that CAS changes rate-base control to doing nothing
  753. 'anti-side-slip-min-speed' : 0.015
  754. },
  755. 'output' : { # Output gains for CAS
  756. 'roll' : 0.06,
  757. 'pitch' : -0.1,
  758. 'yaw' : 0.5,
  759. 'roll-brake-freq' : 10,
  760. 'pitch-brake-freq' : 3,
  761. 'roll-brake' : 0.4,
  762. 'pitch-brake' : 6,
  763. 'anti-side-slip-gain' : -4.5,
  764. 'heading-adjuster-gain' : -5,
  765. 'heading-adjuster-limit' : 5,
  766. },
  767. 'control' : { # configuration for CAS augumentation modes
  768. 'attitude' : { # Attitude control augmentation mode (e.g. ATTDAGMT button on OH-1)
  769. # Note: attitude-control-threshold must be smaller than rate-control-threshold in any mode
  770. 'attitude-control-threshold' : 0.95, # Roll / Pitch attitude(angle) hold mode when 0 < input <= 0.95
  771. 'rate-control-threshold' : 1.0 # Rate hold mode when 0.95 < input <= 1.0
  772. },
  773. 'rate' : { # Rate control augmentation mode
  774. 'attitude-control-threshold' : 0.0,
  775. 'rate-control-threshold' : 0.95
  776. },
  777. }
  778. },
  779. 'sas' : { # gains for SAS
  780. 'roll' : 0.02,
  781. 'pitch' : -0.10,
  782. 'yaw' : 0.04,
  783. 'authority-limit' : 0.15 # How much SAS will take over pilot's control. 0.15 means 15%
  784. },
  785. 'sensitivities' : {
  786. 'roll' : 1.0,
  787. 'pitch' : 1.0,
  788. 'yaw' : 3.0
  789. },
  790. 'tail-rotor' : { # parameters for tail rotor control based on throttle / collective
  791. 'src-minimum' : 0.10, # throttle value that outputs low-limit
  792. 'src-maximum' : 1.00, # throttle value that outputs high-limit
  793. 'low-limit' : 0.00011,
  794. 'high-limit' : 0.0035,
  795. 'error-adjuster-gain' : -0.5, # gain that how much CAS adjust yaw rate
  796. 'authority-limit' : 0.3
  797. },
  798. 'stabilator' : { # gain tables for adjusting either incidence or flap angle of hstab
  799. # index is the speed (Kt) devided by 10
  800. # 0 10 20 30 40 50 60 70 80 90 100 110 120 130 140 150 160, 170, 180, .....
  801. 'gain-table' : [-0.9, -0.8, 0.1, -0.5, 0.0, 0.7, 0.8, 0.9, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.9, 0.8, 0.6, 0.4, 0.2, -1.0]
  802. }
  803. },
  804. 'switches' : { # master switches for AFCS, can be controlled by cockpit panel or keys
  805. 'auto-hover' : 0,
  806. 'cas' : 1,
  807. 'sas' : 1,
  808. 'attitude-control' : 0,
  809. 'auto-stabilator' : 1,
  810. 'sideslip-adjuster' : 1,
  811. 'tail-rotor-adjuster' : 1,
  812. 'heading-adjuster' : 0,
  813. 'air-speed-lock' : 0,
  814. 'heading-lock' : 0,
  815. 'altitude-lock' : 0,
  816. }
  817. };
  818. #
  819. # initialize - creates AFCS components and invokes AFCS main loop
  820. #
  821. var initialize = func {
  822. cas = CAS.new(nil, "/controls/flight/fcs/cas");
  823. afcs = AFCS.new("/controls/flight/fcs/cas", "/controls/flight/fcs/afcs");
  824. sas = SAS.new("/controls/flight/fcs/afcs", "/controls/flight/fcs");
  825. stabilator = Stabilator.new();
  826. tail = TailRotorCollective.new();
  827. backup = BackupFCS.new();
  828. setlistener("/rotors/main/cone-deg", update);
  829. }
  830. #
  831. # Stores default AFCS parameters at startup
  832. #
  833. var confNode = props.globals.getNode("/controls/flight/fcs", 1);
  834. confNode.setValues(default_fcs_params);
  835. #
  836. # fcs-initialized signal must be set by per-aircraft nasal script
  837. # to show that FCS configuration parameters are set
  838. #
  839. setlistener("/sim/signals/fcs-initialized", initialize);