1 # This module provide basic functions and classes for use in aircraft specific
7 # ==============================================================================
9 # creates (if necessary) and returns a property node from arg[0],
10 # which can be a property node already, or a property path
12 var makeNode = func(n) {
13 if (isa(n, props.Node))
16 return props.globals.getNode(n, 1);
20 # returns args[index] if available and non-nil, or default otherwise
22 var optarg = func(args, index, default) {
23 size(args) > index and args[index] != nil ? args[index] : default;
29 # ==============================================================================
30 # class for objects moving at constant speed, with the ability to
31 # reverse moving direction at any point. Appropriate for doors, canopies, etc.
34 # door.new(<property>, <swingtime> [, <startpos>]);
36 # property ... door node: property path or node
37 # swingtime ... time in seconds for full movement (0 -> 1)
38 # startpos ... initial position (default: 0)
41 # ./position-norm (double) (default: <startpos>)
42 # ./enabled (bool) (default: 1)
45 # var canopy = aircraft.door.new("sim/model/foo/canopy", 5);
49 new: func(node, swingtime, pos = 0) {
50 var m = { parents: [door] };
51 m.node = makeNode(node);
52 m.swingtime = swingtime;
53 m.enabledN = m.node.initNode("enabled", 1, "BOOL");
54 m.positionN = m.node.initNode("position-norm", pos);
58 # door.enable(bool) -> set ./enabled
60 me.enabledN.setBoolValue(v);
63 # door.setpos(double) -> set ./position-norm without movement
66 me.positionN.setValue(pos);
67 me.target = pos < 0.5;
70 # double door.getpos() -> return current position as double
72 me.positionN.getValue();
74 # door.close() -> move to closed state
76 me.move(me.target = 0);
78 # door.open() -> move to open state
80 me.move(me.target = 1);
82 # door.toggle() -> move to opposite end position
86 # door.stop() -> stop movement
88 interpolate(me.positionN);
90 # door.move(double) -> move to arbitrary position
92 var pos = me.getpos();
94 var time = abs(pos - target) * me.swingtime;
95 interpolate(me.positionN, target, time);
97 me.target = !me.target;
104 # ==============================================================================
105 # class for generation of pulsing values. Appropriate for controlling
106 # beacons, strobes, etc.
109 # light.new(<property>, <pattern> [, <switch>]);
110 # light.new(<property>, <stretch>, <pattern> [, <switch>]);
112 # property ... light node: property path or node
113 # stretch ... multiplicator for all pattern values
114 # pattern ... array of on/off time intervals (in seconds)
115 # switch ... property path or node to use as switch (default: ./enabled)
116 # instead of ./enabled
119 # ./state (bool) (default: 0)
120 # ./enabled (bool) (default: 0) except if <switch> given)
123 # aircraft.light.new("sim/model/foo/beacon", [0.4, 0.4]); # anonymous light
125 # var strobe = aircraft.light.new("sim/model/foo/strobe", [0.05, 0.05, 0.05, 1],
126 # "controls/lighting/strobe");
129 # var switch = props.globals.getNode("controls/lighting/strobe", 1);
130 # var pattern = [0.02, 0.03, 0.02, 1];
131 # aircraft.light.new("sim/model/foo/strobe-top", 1.001, pattern, switch);
132 # aircraft.light.new("sim/model/foo/strobe-bot", 1.005, pattern, switch);
136 var m = { parents: [light] };
137 m.node = makeNode(arg[0]);
140 if (typeof(arg[c]) == "scalar") {
146 if (size(arg) > c and arg[c] != nil)
147 m.switchN = makeNode(arg[c]);
149 m.switchN = m.node.getNode("enabled", 1);
151 m.switchN.initNode(nil, 0, "BOOL");
152 m.stateN = m.node.initNode("state", 0, "BOOL");
154 forindex (var i; m.pattern)
155 m.pattern[i] *= stretch;
164 m.switchL = setlistener(m.switchN, func m._switch_(), 1);
169 removelistener(me.switchL);
171 # light.switch(bool) -> set light switch (also affects other lights
172 # that use the same switch)
174 me.switchN.setBoolValue(v);
177 # light.toggle() -> toggle light switch
179 me.switchN.setBoolValue(!me.switchN.getValue());
182 # light.cont() -> continuous light
184 if (!me.continuous) {
187 me.stateN.setBoolValue(me.lastswitch);
191 # light.blink() -> blinking light (default)
192 # light.blink(3) -> when switched on, only run three blink sequences;
193 # second optional arg defines state after the sequences
194 blink: func(count = -1, endstate = 0) {
196 me.endstate = endstate;
200 me.stateN.setBoolValue(0);
201 me.lastswitch and me._loop_(me.loopid += 1);
206 var switch = me.switchN.getBoolValue();
207 switch != me.lastswitch or return;
208 me.lastswitch = switch;
210 if (me.continuous or !switch) {
211 me.stateN.setBoolValue(switch);
213 me.stateN.setBoolValue(0);
215 me.count = me.seqcount;
216 me._loop_(me.loopid);
220 id == me.loopid or return;
223 me.stateN.setBoolValue(me.endstate);
226 me.stateN.setBoolValue(me.index == 2 * int(me.index / 2));
227 settimer(func me._loop_(id), me.pattern[me.index]);
228 if ((me.index += 1) >= size(me.pattern)) {
239 # ==============================================================================
240 # class that implements a variable-interval EWMA (Exponentially Weighted
241 # Moving Average) lowpass filter with characteristics independent of the
245 # lowpass.new(<coefficient>);
248 # var lp = aircraft.lowpass.new(1.5);
249 # print(lp.filter(10)); # prints 10
250 # print(lp.filter(0));
254 var m = { parents: [lowpass] };
255 m.coeff = coeff >= 0 ? coeff : die("aircraft.lowpass(): coefficient must be >= 0");
259 # filter(raw_value) -> push new value, returns filtered value
261 me.filter = me._filter_;
264 # get() -> returns filtered value
268 # set() -> sets new average and returns it
273 var dt = getprop("/sim/time/delta-sec")*getprop("/sim/speed-up");
274 var c = dt / (me.coeff + dt);
275 me.value = v * c + me.value * (1 - c);
282 # ==============================================================================
283 # same as above, but for angles. Filters sin/cos separately and calculates the
284 # angle again from them. This avoids unexpected jumps from 179.99 to -180 degree.
286 var angular_lowpass = {
288 var m = { parents: [angular_lowpass] };
289 m.sin = lowpass.new(coeff);
290 m.cos = lowpass.new(coeff);
296 me.value = math.atan2(me.sin.filter(math.sin(v)), me.cos.filter(math.cos(v))) * R2D;
300 me.sin.set(math.sin(v));
301 me.cos.set(math.cos(v));
311 # ==============================================================================
312 # class that loads and saves properties to aircraft-specific data files in
313 # ~/.fgfs/aircraft-data/ (Unix) or %APPDATA%\flightgear.org\aircraft-data\.
314 # There's no public constructor, as the only needed instance gets created
318 # data.add(<properties>);
319 # data.save([<interval>])
321 # properties ... about any combination of property nodes (props.Node)
322 # or path name strings, or lists or hashes of them,
323 # lists of lists of them, etc.
324 # interval ... save in <interval> minutes intervals, or only once
325 # if 'nil' or empty (and again at reinit/exit)
328 # /sim/signals/save ... set to 'true' right before saving. Can be used
329 # to update values that are to be saved
332 # var p = props.globals.getNode("/sim/model", 1);
334 # var hash = {"foo": p, "bar": p};
337 # aircraft.data.add("/sim/fg-root", p, "/sim/fg-home");
338 # aircraft.data.add(p, vec, hash, "/sim/fg-root");
340 # # now save only once (and at exit/reinit, which is automatically done)
341 # aircraft.data.save();
343 # # or save now and every 30 sec (and at exit/reinit)
344 # aircraft.data.save(0.5);
348 me.path = getprop("/sim/fg-home") ~ "/aircraft-data/" ~ getprop("/sim/aircraft") ~ ".xml";
349 me.signalN = props.globals.getNode("/sim/signals/save", 1);
354 setlistener("/sim/signals/reinit", func(n) { n.getBoolValue() and me._save_() });
355 setlistener("/sim/signals/exit", func me._save_());
358 if (io.stat(me.path) != nil) {
359 printlog("info", "loading aircraft data from ", me.path);
360 io.read_properties(me.path, props.globals);
363 save: func(v = nil) {
368 me.interval = 60 * v;
369 me._loop_(me.loopid);
373 id == me.loopid or return;
375 settimer(func me._loop_(id), me.interval);
378 size(me.catalog) or return;
379 printlog("debug", "saving aircraft data to ", me.path);
380 me.signalN.setBoolValue(1);
381 var data = props.Node.new();
382 foreach (var c; me.catalog) {
386 props.copy(props.globals.getNode(c, 1), data.getNode(c, 1));
388 io.write_properties(me.path, data);
391 foreach (var n; props.nodeList(p))
392 append(me.catalog, n.getPath());
399 # ==============================================================================
400 # class that implements timer that can be started, stopped, reset, and can
401 # have its value saved to the aircraft specific data file. Saving the value
402 # is done automatically by the aircraft.Data class.
405 # timer.new(<property> [, <resolution:double> [, <save:bool>]])
407 # <property> ... property path or props.Node hash that holds the timer value
408 # <resolution> ... timer update resolution -- interval in seconds in which the
409 # timer property is updated while running (default: 1 s)
410 # <save> ... bool that defines whether the timer value should be saved
411 # and restored next time, as needed for Hobbs meters
415 # var hobbs_turbine = aircraft.timer.new("/sim/time/hobbs/turbine[0]", 60);
416 # hobbs_turbine.start();
418 # aircraft.timer.new("/sim/time/hobbs/battery", 60).start(); # anonymous timer
421 new: func(prop, res = 1, save = 1) {
422 var m = { parents: [timer] };
423 m.node = makeNode(prop);
424 if (m.node.getType() == "NONE")
425 m.node.setDoubleValue(0);
427 me.systimeN = props.globals.getNode("/sim/time/elapsed-sec", 1);
428 m.last_systime = nil;
432 m.reinitL = setlistener("/sim/signals/reinit", func(n) {
435 m.total = m.node.getValue();
437 m.node.setDoubleValue(m.total);
442 m.saveL = setlistener("/sim/signals/save", func m._save_());
450 removelistener(me.reinitL);
452 removelistener(me.saveL);
455 me.running and return;
456 me.last_systime = me.systimeN.getValue();
457 if (me.interval != nil)
458 me._loop_(me.loopid);
463 me.running or return;
470 me.node.setDoubleValue(0);
471 me.last_systime = me.systimeN.getValue();
474 var sys = me.systimeN.getValue();
475 me.node.setDoubleValue(me.node.getValue() + sys - me.last_systime);
476 me.last_systime = sys;
483 id != me.loopid and return;
485 settimer(func me._loop_(id), me.interval);
492 # =============================================================================
493 # Class that maintains livery XML files (see English Electric Lightning for an
494 # example). The last used livery is saved on exit and restored next time. Livery
495 # files are regular PropertyList XML files whose properties are copied to the
499 # livery.init(<livery-dir> [, <name-path> [, <sort-path>]]);
501 # <livery-dir> ... directory with livery XML files, relative to $FG_ROOT
502 # <name-path> ... property path to the livery name in the livery files
503 # and the property tree (default: sim/model/livery/name)
504 # <sort-path> ... property path to the sort criterion (default: same as
505 # <name-path> -- that is: alphabetic sorting)
508 # aircraft.livery.init("Aircraft/Lightning/Models/Liveries",
509 # "sim/model/livery/variant",
510 # "sim/model/livery/index"); # optional
512 # aircraft.livery.dialog.toggle();
513 # aircraft.livery.select("OEBH");
514 # aircraft.livery.next();
517 init: func(dir, nameprop = "sim/model/livery/name", sortprop = nil) {
518 me.parents = [gui.OverlaySelector.new("Select Livery", dir, nameprop,
519 sortprop, "sim/model/livery/file")];
520 me.dialog = me.parents[0];
527 # =============================================================================
528 # Class for maintaining liveries in MP aircraft. It is used in Nasal code that's
529 # embedded in aircraft animation XML files, and checks in intervals whether the
530 # parent aircraft has changed livery, in which case it changes the livery
531 # in the remote aircraft accordingly. This class is a wrapper for overlay_update.
534 # livery_update.new(<livery-dir> [, <interval:10> [, <func>]]);
536 # <livery-dir> ... directory with livery files, relative to $FG_ROOT
537 # <interval> ... checking interval in seconds (default: 10)
538 # <func> ... callback function that's called with the ./sim/model/livery/file
539 # contents as argument whenever the livery has changed. This can
540 # be used for post-processing.
545 # var livery_update = aircraft.livery_update.new(
546 # "Aircraft/R22/Models/Liveries", 30,
547 # func print("R22 livery update"));
551 # livery_update.stop();
555 var livery_update = {
556 new: func(liveriesdir, interval = 10.01, callback = nil) {
557 var m = { parents: [livery_update, overlay_update.new()] };
558 m.parents[1].add(liveriesdir, "sim/model/livery/file", callback);
559 m.parents[1].interval = interval;
563 me.parents[1].stop();
570 # =============================================================================
571 # Class for maintaining overlays in MP aircraft. It is used in Nasal code that's
572 # embedded in aircraft animation XML files, and checks in intervals whether the
573 # parent aircraft has changed an overlay, in which case it copies the respective
574 # overlay to the aircraft's root directory.
577 # livery_update.new();
578 # livery_update.add(<overlay-dir>, <property> [, <callback>]);
580 # <overlay-dir> ... directory with overlay files, relative to $FG_ROOT
581 # <property> ... MP property where the overlay file name can be found
582 # (usually one of the sim/multiplay/generic/string properties)
583 # <callback> ... callback function that's called with two arguments:
584 # the file name (without extension) and the overlay directory
589 # var update = aircraft.overlay_update.new();
590 # update.add("Aircraft/F4U/Models/Logos", "sim/multiplay/generic/string");
598 var overlay_update = {
600 var m = { parents: [overlay_update] };
604 if (m.root.getName() == "multiplayer")
608 add: func(path, prop, callback = nil) {
609 var path = path ~ '/';
610 me.data[path] = [me.root.initNode(prop, ""), "",
611 typeof(callback) == "func" ? callback : func nil];
615 me._loop_ = func nil;
618 foreach (var path; keys(me.data)) {
619 var v = me.data[path];
620 var file = v[0].getValue();
622 io.read_properties(path ~ file ~ ".xml", me.root);
623 v[2](v[1] = file, path);
626 settimer(func me._loop_(), me.interval);
633 # =============================================================================
634 # Class that implements differential braking depending on rudder position.
635 # Note that this overrides the controls.applyBrakes() wrapper. If you need
636 # your own version, then override it again after the steering.init() call.
639 # steering.init([<property> [, <threshold>]]);
641 # <property> ... property path or props.Node hash that enables/disables
642 # brake steering (usually bound to the js trigger button)
643 # <threshold> ... defines range (+- threshold) around neutral rudder
644 # position in which both brakes are applied
647 # aircraft.steering.init("/controls/gear/steering", 0.2);
648 # aircraft.steering.init();
651 init: func(switch = "/controls/gear/brake-steering", threshold = 0.3) {
652 me.threshold = threshold;
653 me.switchN = makeNode(switch);
654 me.switchN.setBoolValue(me.switchN.getBoolValue());
655 me.leftN = props.globals.getNode("/controls/gear/brake-left", 1);
656 me.rightN = props.globals.getNode("/controls/gear/brake-right", 1);
657 me.rudderN = props.globals.getNode("/controls/flight/rudder", 1);
660 controls.applyBrakes = func(v, w = 0) {
662 steering.leftN.setValue(v);
664 steering.rightN.setValue(v);
666 steering.switchN.setValue(v);
668 setlistener(me.switchN, func(n) {
671 me._loop_(me.loopid);
677 id == me.loopid or return;
678 var rudder = me.rudderN.getValue();
679 if (rudder > me.threshold)
680 me.setbrakes(0, rudder);
681 elsif (rudder < -me.threshold)
682 me.setbrakes(-rudder, 0);
686 settimer(func me._loop_(id), 0);
688 setbrakes: func(left, right) {
689 me.leftN.setDoubleValue(left);
690 me.rightN.setDoubleValue(right);
697 # =============================================================================
698 # Singleton class that supports quick trimming and compensates for the lack
699 # of resistance/force feedback in most joysticks. Normally the pilot trims such
700 # that no real or artificially generated (by means of servo motors and spring
701 # preloading) forces act on the stick/yoke and it is in a comfortable position.
702 # This doesn't work well on computer joysticks.
705 # autotrim.start(); # on key/button press
706 # autotrim.stop(); # on key/button release (mod-up)
709 # (1) move the stick such that the aircraft is in an orientation that
710 # you want to trim for (forward flight, hover, ...)
711 # (2) press autotrim button and keep it pressed
712 # (3) move stick/yoke to neutral position (center)
713 # (4) release autotrim button
717 me.elevator = me.Trim.new("elevator");
718 me.aileron = me.Trim.new("aileron");
719 me.rudder = me.Trim.new("rudder");
724 me.active and return;
729 me._loop_(me.loopid += 1);
738 id == me.loopid or return;
740 settimer(func me._loop_(id), 0);
743 me.elevator.update();
749 var m = { parents: [autotrim.Trim] };
750 m.trimN = props.globals.getNode("/controls/flight/" ~ name ~ "-trim", 1);
751 m.ctrlN = props.globals.getNode("/controls/flight/" ~ name, 1);
755 me.last = me.ctrlN.getValue();
758 var v = me.ctrlN.getValue();
759 me.trimN.setDoubleValue(me.trimN.getValue() + me.last - v);
768 # =============================================================================
769 # Provides a property which can be used to contol particles used to simulate tyre
770 # smoke on landing. Weight on wheels, vertical speed, ground speed, ground friction
771 # factor are taken into account. Tyre slip is simulated by low pass filters.
773 # Modifications to the model file are required.
775 # Generic XML particle files are available, but are not mandatory
776 # (see Hawker Seahawk for an example).
779 # aircraft.tyresmoke.new(gear index [, auto = 0])
780 # gear index - the index of the gear to which the tyre smoke is attached
781 # auto - enable automatic update (recommended). defaults to 0 for backward compatibility.
782 # aircraft.tyresmoke.del()
784 # aircraft.tyresmoke.update()
785 # Runs the update. Not required if automatic updates are enabled.
788 # var tyresmoke_0 = aircraft.tyresmoke.new(0);
789 # tyresmoke_0.update();
793 # number: index of gear to be animated, i.e. "2" for /gear/gear[2]/...
795 # auto: 1 when tyresmoke should start on update loop. 0 when you're going
796 # to call the update method from one of your own loops.
798 # diff_norm: value adjusting the necessary percental change of roll-speed
799 # to trigger tyre smoke. Default value is 0.05. More realistic results can
800 # be achieved with significantly higher values (i.e. use 0.8).
802 # check_vspeed: 1 when tyre smoke should only be triggered when vspeed is negative
803 # (usually doesn't work for all gear, since vspeed=0.0 after the first gear touches
804 # ground). Use 0 to make tyre smoke independent of vspeed.
805 # Note: in reality, tyre smoke doesn't depend on vspeed, but only on acceleration
810 new: func(number, auto = 0, diff_norm = 0.05, check_vspeed=1) {
811 var m = { parents: [tyresmoke] };
812 m.vertical_speed = (!check_vspeed) ? nil : props.globals.initNode("velocities/vertical-speed-fps");
813 m.diff_norm = diff_norm;
814 m.speed = props.globals.initNode("velocities/groundspeed-kt");
815 m.rain = props.globals.initNode("environment/metar/rain-norm");
817 var gear = props.globals.getNode("gear/gear[" ~ number ~ "]/");
818 m.wow = gear.initNode("wow");
819 m.tyresmoke = gear.initNode("tyre-smoke", 0, "BOOL");
820 m.friction_factor = gear.initNode("ground-friction-factor", 1);
821 m.sprayspeed = gear.initNode("sprayspeed-ms");
822 m.spray = gear.initNode("spray", 0, "BOOL");
823 m.spraydensity = gear.initNode("spray-density", 0, "DOUBLE");
827 if (getprop("sim/flight-model") == "jsb") {
828 var wheel_speed = "fdm/jsbsim/gear/unit[" ~ number ~ "]/wheel-speed-fps";
829 m.rollspeed = props.globals.initNode(wheel_speed);
830 m.get_rollspeed = func m.rollspeed.getValue() * 0.3043;
832 m.rollspeed = gear.initNode("rollspeed-ms");
833 m.get_rollspeed = func m.rollspeed.getValue();
836 m.lp = lowpass.new(2);
841 if (me.listener != nil) {
842 removelistener(me.listener);
848 var rollspeed = me.get_rollspeed();
849 var vert_speed = (me.vertical_speed) != nil ? me.vertical_speed.getValue() : -999;
850 var groundspeed = me.speed.getValue();
851 var friction_factor = me.friction_factor.getValue();
852 var wow = me.wow.getValue();
853 var rain = me.rain.getValue();
855 var filtered_rollspeed = me.lp.filter(rollspeed);
856 var diff = math.abs(rollspeed - filtered_rollspeed);
857 var diff_norm = diff > 0 ? diff / rollspeed : 0;
859 if (wow and vert_speed < -1.2
860 and diff_norm > me.diff_norm
861 and friction_factor > 0.7 and groundspeed > 50
863 me.tyresmoke.setValue(1);
864 me.spray.setValue(0);
865 me.spraydensity.setValue(0);
866 } elsif (wow and groundspeed > 5 and rain >= 0.20) {
867 me.tyresmoke.setValue(0);
868 me.spray.setValue(1);
869 me.sprayspeed.setValue(rollspeed * 6);
870 me.spraydensity.setValue(rain * groundspeed);
872 me.tyresmoke.setValue(0);
873 me.spray.setValue(0);
874 me.sprayspeed.setValue(0);
875 me.spraydensity.setValue(0);
879 settimer(func me.update(), 0);
880 if (me.listener != nil) {
881 removelistener(me.listener);
884 } elsif (me.listener == nil) {
885 me.listener = setlistener(me.wow, func me._wowchanged_(), 0, 0);
889 _wowchanged_: func() {
890 if (me.wow.getValue()) {
898 # =============================================================================
899 # Helper class to contain the tyresmoke objects for all the gears.
900 # Will update automatically, nothing else needs to be done by the caller.
903 # aircraft.tyresmoke_system.new(<gear index 1>, <gear index 2>, ...)
904 # <gear index> - the index of the gear to which the tyre smoke is attached
905 # aircraft.tyresmoke_system.del()
908 # var tyresmoke_system = aircraft.tyresmoke_system.new(0, 1, 2, 3, 4);
910 var tyresmoke_system = {
912 var m = { parents: [tyresmoke_system] };
913 # preset array to proper size
915 setsize(m.gears, size(arg));
916 for(var i = size(arg) - 1; i >= 0; i -= 1) {
917 m.gears[i] = tyresmoke.new(arg[i], 1);
922 foreach(var gear; me.gears) {
929 # =============================================================================
930 # Provides a property which can be used to control rain. Can be used to turn
931 # off rain in internal views, and or used with a texture on canopies etc.
932 # The output is co-ordinated with system precipitation:
934 # /sim/model/rain/raining-norm rain intensity
935 # /sim/model/rain/flow-mps drop flow speed [m/s]
937 # See Hawker Seahawk for an example.
940 # aircraft.rain.init();
941 # aircraft.rain.update();
945 me.elapsed_timeN = props.globals.getNode("sim/time/elapsed-sec");
946 me.dtN = props.globals.getNode("sim/time/delta-sec");
948 me.enableN = props.globals.initNode("sim/rendering/precipitation-aircraft-enable", 0, "BOOL");
949 me.precip_levelN = props.globals.initNode("environment/params/precipitation-level-ft", 0);
950 me.altitudeN = props.globals.initNode("position/altitude-ft", 0);
951 me.iasN = props.globals.initNode("velocities/airspeed-kt", 0);
952 me.rainingN = props.globals.initNode("sim/model/rain/raining-norm", 0);
953 me.flowN = props.globals.initNode("sim/model/rain/flow-mps", 0);
955 var canopyN = props.globals.initNode("gear/canopy/position-norm", 0);
956 var thresholdN = props.globals.initNode("sim/model/rain/flow-threshold-kt", 15);
958 setlistener(canopyN, func(n) me.canopy = n.getValue(), 1, 0);
959 setlistener(thresholdN, func(n) me.threshold = n.getValue(), 1);
960 setlistener("sim/rendering/precipitation-gui-enable", func(n) me.enabled = n.getValue(), 1);
961 setlistener("environment/metar/rain-norm", func(n) me.rain = n.getValue(), 1);
962 setlistener("sim/current-view/internal", func(n) me.internal = n.getValue(), 1);
965 var altitude = me.altitudeN.getValue();
966 var precip_level = me.precip_levelN.getValue();
968 if (me.enabled and me.internal and altitude < precip_level and me.canopy < 0.001) {
969 var time = me.elapsed_timeN.getValue();
970 var ias = me.iasN.getValue();
971 var dt = me.dtN.getValue();
973 me.flowN.setDoubleValue(ias < me.threshold ? 0 : time * 0.5 + ias * NM2M * dt / 3600);
974 me.rainingN.setDoubleValue(me.rain);
975 if (me.enableN.getBoolValue())
976 me.enableN.setBoolValue(0);
978 me.flowN.setDoubleValue(0);
979 me.rainingN.setDoubleValue(0);
980 if (me.enableN.getBoolValue() != 1)
981 me.enableN.setBoolValue(1);
989 # =============================================================================
990 # Usage: aircraft.teleport(lat:48.3, lon:32.4, alt:5000);
992 var teleport = func(airport = "", runway = "", lat = -9999, lon = -9999, alt = 0,
993 speed = 0, distance = 0, azimuth = 0, glideslope = 0, heading = 9999) {
994 setprop("/sim/presets/airport-id", airport);
995 setprop("/sim/presets/runway", runway);
996 setprop("/sim/presets/parkpos", "");
997 setprop("/sim/presets/latitude-deg", lat);
998 setprop("/sim/presets/longitude-deg", lon);
999 setprop("/sim/presets/altitude-ft", alt);
1000 setprop("/sim/presets/airspeed-kt", speed);
1001 setprop("/sim/presets/offset-distance-nm", distance);
1002 setprop("/sim/presets/offset-azimuth-nm", azimuth);
1003 setprop("/sim/presets/glideslope-deg", glideslope);
1004 setprop("/sim/presets/heading-deg", heading);
1005 fgcommand("reposition");
1010 # returns wind speed [kt] from given direction [deg]; useful for head-wind
1012 var wind_speed_from = func(azimuth) {
1013 var dir = (getprop("/environment/wind-from-heading-deg") - azimuth) * D2R;
1014 return getprop("/environment/wind-speed-kt") * math.cos(dir);
1019 # returns true airspeed for given indicated airspeed [kt] and altitude [m]
1021 var kias_to_ktas = func(kias, altitude) {
1022 var seapress = getprop("/environment/pressure-sea-level-inhg");
1023 var seatemp = getprop("/environment/temperature-sea-level-degc");
1024 var coralt_ft = altitude * M2FT + (29.92 - seapress) * 910;
1025 return kias * (1 + 0.00232848233 * (seatemp - 15))
1026 * (1.0025 + coralt_ft * (0.0000153
1027 - kias * (coralt_ft * 0.0000000000003 + 0.0000000045)
1028 + (0.0000119 * (math.exp(coralt_ft * 0.000016) - 1))));
1033 # HUD control class to handle both HUD implementations
1034 # ==============================================================================
1038 me.vis1N = props.globals.getNode("/sim/hud/visibility[1]", 1);
1039 me.currcolN = props.globals.getNode("/sim/hud/current-color", 1);
1040 me.currentPathN = props.globals.getNode("/sim/hud/current-path", 1);
1041 me.hudN = props.globals.getNode("/sim/hud", 1);
1042 me.paletteN = props.globals.getNode("/sim/hud/palette", 1);
1043 me.brightnessN = props.globals.getNode("/sim/hud/color/brightness", 1);
1044 me.currentN = me.vis1N;
1046 # keep compatibility with earlier version of FG - hud/path[1] is
1048 me.currentPathN.setIntValue(1);
1050 cycle_color: func { # h-key
1051 if (!me.currentN.getBoolValue()) # if off, turn on
1052 return me.currentN.setBoolValue(1);
1054 var i = me.currcolN.getValue() + 1; # if through, turn off
1055 if (i < 0 or i >= size(me.paletteN.getChildren("color"))) {
1056 me.currentN.setBoolValue(0);
1057 me.currcolN.setIntValue(0);
1058 } else { # otherwise change color
1059 me.currentN.setBoolValue(1);
1060 me.currcolN.setIntValue(i);
1063 cycle_brightness: func { # H-key
1064 me.is_active() or return;
1065 var br = me.brightnessN.getValue() - 0.2;
1066 me.brightnessN.setValue(br > 0.01 ? br : 1);
1068 normal_type: func { # i-key
1069 me.currentPathN.setIntValue(1);
1071 cycle_type: func { # I-key
1072 var i = me.currentPathN.getValue() + 1;
1073 if (i < 1 or i > size(me.hudN.getChildren("path"))) {
1075 me.currentPathN.setIntValue(1);
1077 me.currentPathN.setIntValue(i);
1081 me.vis1N.getValue();
1086 # =============================================================================
1087 # class that creates a fuel tank cross-feed valve. Designed for YASim aircraft;
1088 # JSBSim aircraft can simply use systems code within the FDM (see 747-400 for
1091 # WARNING: this class requires the tank properties to be ready, so call new()
1092 # after the FDM is initialized.
1095 # crossfeed_valve.new(<max_flow_rate>, <property>, <tank>, <tank>, ... );
1096 # crossfeed_valve.open(<update>);
1097 # crossfeed_valve.close(<update>);
1099 # <max_flow_rate> ... maximum transfer rate between the tanks in lbs/sec
1100 # <property> ... property path to use as switch - pass nil to use no such switch
1101 # <tank> ... number of a tank to connect - can have unlimited number of tanks connected
1102 # <update> ... update switch property when opening/closing valve via Nasal - 0 or 1; by default, 1
1106 # aircraft.crossfeed_valve.new(0.5, "/controls/fuel/x-feed", 0, 1, 2);
1108 # var xfeed = aircraft.crossfeed_valve.new(1, nil, 0, 1);
1111 var crossfeed_valve = {
1112 new: func(flow_rate, path) {
1113 var m = { parents: [crossfeed_valve] };
1117 m.flow_rate = flow_rate;
1119 m.switch_node = props.globals.initNode(path, 0, "BOOL");
1120 setlistener(path, func(node) {
1121 if (node.getBoolValue()) m.open(0);
1126 for (var i = 0; i < size(arg); i += 1) {
1127 var tank = props.globals.getNode("consumables/fuel/tank[" ~ arg[i] ~ "]");
1128 if (tank.getChild("level-lbs") != nil) append(m.tanks, tank);
1132 open: func(update_prop = 1) {
1133 if (me.valve_open == 1) return;
1134 if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(1);
1137 settimer(func me._loop_(me.loopid), me.interval);
1139 close: func(update_prop = 1) {
1140 if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(0);
1144 if (id != me.loopid) return;
1145 var average_level = 0;
1146 var count = size(me.tanks);
1147 for (var i = 0; i < count; i += 1) {
1148 var level_node = me.tanks[i].getChild("level-lbs");
1149 average_level += level_node.getValue();
1151 average_level /= size(me.tanks);
1152 var highest_diff = 0;
1153 for (var i = 0; i < count; i += 1) {
1154 var level = me.tanks[i].getChild("level-lbs").getValue();
1155 var diff = math.abs(average_level - level);
1156 if (diff > highest_diff) highest_diff = diff;
1158 for (var i = 0; i < count; i += 1) {
1159 var level_node = me.tanks[i].getChild("level-lbs");
1160 var capacity = me.tanks[i].getChild("capacity-gal_us").getValue() * me.tanks[i].getChild("density-ppg").getValue();
1161 var diff = math.abs(average_level - level_node.getValue());
1162 var min_level = math.max(0, level_node.getValue() - me.flow_rate * diff / highest_diff);
1163 var max_level = math.min(capacity, level_node.getValue() + me.flow_rate * diff / highest_diff);
1164 var level = level_node.getValue() > average_level ? math.max(min_level, average_level) : math.min(max_level, average_level);
1165 level_node.setValue(level);
1167 if (me.valve_open) settimer(func me._loop_(id), me.interval);
1174 # module initialization
1175 # ==============================================================================
1177 _setlistener("/sim/signals/nasal-dir-initialized", func {
1178 props.globals.initNode("/sim/time/elapsed-sec", 0);
1179 props.globals.initNode("/sim/time/delta-sec", 0);
1180 props.globals.initNode("/sim/time/delta-realtime-sec", 0.00000001);
1186 ##### temporary hack to provide backward compatibility for /sim/auto-coordination
1187 ##### remove this code when all references to /sim/auto-coordination are gone
1188 var ac = props.globals.getNode("/sim/auto-coordination");
1191 "WARNING: using deprecated property /sim/auto-coordination. Please change to /controls/flight/auto-coordination" );
1192 ac.alias(props.globals.getNode("/controls/flight/auto-coordination", 1));
1194 #### end of temporary hack for /sim/auto-coordination
1196 if (!getprop("/sim/startup/restore-defaults")) {
1197 # load user-specific aircraft settings
1199 var n = props.globals.getNode("/sim/aircraft-data");
1201 foreach (var c; n.getChildren("path"))
1202 if (c.getType() != "NONE")
1203 data.add(c.getValue());
1205 if (!getprop("/sim/startup/save-on-exit"))
1208 data._save_ = func nil;
1209 data._loop_ = func nil;