1# This module provide basic functions and classes for use in aircraft specific 2# Nasal context. 3 4 5 6# helper functions 7# ============================================================================== 8 9# creates (if necessary) and returns a property node from arg[0], 10# which can be a property node already, or a property path 11# 12var makeNode = func(n) { 13 if (isa(n, props.Node)) 14 return n; 15 else 16 return props.globals.getNode(n, 1); 17} 18 19 20# returns args[index] if available and non-nil, or default otherwise 21# 22var optarg = func(args, index, default) { 23 size(args) > index and args[index] != nil ? args[index] : default; 24} 25 26 27 28# door 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. 32# 33# SYNOPSIS: 34# door.new(<property>, <swingtime> [, <startpos>]); 35# 36# property ... door node: property path or node 37# swingtime ... time in seconds for full movement (0 -> 1) 38# startpos ... initial position (default: 0) 39# 40# PROPERTIES: 41# ./position-norm (double) (default: <startpos>) 42# ./enabled (bool) (default: 1) 43# 44# EXAMPLE: 45# var canopy = aircraft.door.new("sim/model/foo/canopy", 5); 46# canopy.open(); 47# 48var door = { 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); 55 m.target = pos < 0.5; 56 return m; 57 }, 58 # door.enable(bool) -> set ./enabled 59 enable: func(v) { 60 me.enabledN.setBoolValue(v); 61 me; 62 }, 63 # door.setpos(double) -> set ./position-norm without movement 64 setpos: func(pos) { 65 me.stop(); 66 me.positionN.setValue(pos); 67 me.target = pos < 0.5; 68 me; 69 }, 70 # double door.getpos() -> return current position as double 71 getpos: func { 72 me.positionN.getValue(); 73 }, 74 # door.close() -> move to closed state 75 close: func { 76 me.move(me.target = 0); 77 }, 78 # door.open() -> move to open state 79 open: func { 80 me.move(me.target = 1); 81 }, 82 # door.toggle() -> move to opposite end position 83 toggle: func { 84 me.move(me.target); 85 }, 86 # door.stop() -> stop movement 87 stop: func { 88 interpolate(me.positionN); 89 }, 90 # door.move(double) -> move to arbitrary position 91 move: func(target) { 92 var pos = me.getpos(); 93 if (pos != target) { 94 var time = abs(pos - target) * me.swingtime; 95 interpolate(me.positionN, target, time); 96 } 97 me.target = !me.target; 98 }, 99}; 100 101 102 103# light 104# ============================================================================== 105# class for generation of pulsing values. Appropriate for controlling 106# beacons, strobes, etc. 107# 108# SYNOPSIS: 109# light.new(<property>, <pattern> [, <switch>]); 110# light.new(<property>, <stretch>, <pattern> [, <switch>]); 111# 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 117# 118# PROPERTIES: 119# ./state (bool) (default: 0) 120# ./enabled (bool) (default: 0) except if <switch> given) 121# 122# EXAMPLES: 123# aircraft.light.new("sim/model/foo/beacon", [0.4, 0.4]); # anonymous light 124#------- 125# var strobe = aircraft.light.new("sim/model/foo/strobe", [0.05, 0.05, 0.05, 1], 126# "controls/lighting/strobe"); 127# strobe.switch(1); 128#------- 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); 133# 134var light = { 135 new: func { 136 var m = { parents: [light] }; 137 m.node = makeNode(arg[0]); 138 var stretch = 1.0; 139 var c = 1; 140 if (isscalar(arg[c])) { 141 stretch = arg[c]; 142 c += 1; 143 } 144 m.pattern = arg[c]; 145 c += 1; 146 if (size(arg) > c and arg[c] != nil) 147 m.switchN = makeNode(arg[c]); 148 else 149 m.switchN = m.node.getNode("enabled", 1); 150 151 m.switchN.initNode(nil, 0, "BOOL"); 152 m.stateN = m.node.initNode("state", 0, "BOOL"); 153 154 forindex (var i; m.pattern) 155 m.pattern[i] *= stretch; 156 157 m.index = 0; 158 m.loopid = 0; 159 m.continuous = 0; 160 m.lastswitch = 0; 161 m.seqcount = -1; 162 m.endstate = 0; 163 m.count = nil; 164 m.switchL = setlistener(m.switchN, func m._switch_(), 1); 165 return m; 166 }, 167 # class destructor 168 del: func { 169 removelistener(me.switchL); 170 }, 171 # light.switch(bool) -> set light switch (also affects other lights 172 # that use the same switch) 173 switch: func(v) { 174 me.switchN.setBoolValue(v); 175 me; 176 }, 177 # light.toggle() -> toggle light switch 178 toggle: func { 179 me.switchN.setBoolValue(!me.switchN.getValue()); 180 me; 181 }, 182 # light.cont() -> continuous light 183 cont: func { 184 if (!me.continuous) { 185 me.continuous = 1; 186 me.loopid += 1; 187 me.stateN.setBoolValue(me.lastswitch); 188 } 189 me; 190 }, 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) { 195 me.seqcount = count; 196 me.endstate = endstate; 197 if (me.continuous) { 198 me.continuous = 0; 199 me.index = 0; 200 me.stateN.setBoolValue(0); 201 me.lastswitch and me._loop_(me.loopid += 1); 202 } 203 me; 204 }, 205 _switch_: func { 206 var switch = me.switchN.getBoolValue(); 207 switch != me.lastswitch or return; 208 me.lastswitch = switch; 209 me.loopid += 1; 210 if (me.continuous or !switch) { 211 me.stateN.setBoolValue(switch); 212 } elsif (switch) { 213 me.stateN.setBoolValue(0); 214 me.index = 0; 215 me.count = me.seqcount; 216 me._loop_(me.loopid); 217 } 218 }, 219 _loop_: func(id) { 220 id == me.loopid or return; 221 if (!me.count) { 222 me.loopid += 1; 223 me.stateN.setBoolValue(me.endstate); 224 return; 225 } 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)) { 229 me.index = 0; 230 if (me.count > 0) 231 me.count -= 1; 232 } 233 }, 234}; 235 236 237 238# lowpass 239# ============================================================================== 240# class that implements a variable-interval EWMA (Exponentially Weighted 241# Moving Average) lowpass filter with characteristics independent of the 242# frame rate. 243# 244# SYNOPSIS: 245# lowpass.new(<coefficient>); 246# 247# EXAMPLE: 248# var lp = aircraft.lowpass.new(1.5); 249# print(lp.filter(10)); # prints 10 250# print(lp.filter(0)); 251# 252var lowpass = { 253 new: func(coeff) { 254 var m = { parents: [lowpass] }; 255 m.coeff = coeff >= 0 ? coeff : die("aircraft.lowpass(): coefficient must be >= 0"); 256 m.value = nil; 257 return m; 258 }, 259 # filter(raw_value) -> push new value, returns filtered value 260 filter: func(v) { 261 me.filter = me._filter_; 262 me.value = v; 263 }, 264 # get() -> returns filtered value 265 get: func { 266 me.value; 267 }, 268 # set() -> sets new average and returns it 269 set: func(v) { 270 me.value = v; 271 }, 272 _filter_: func(v) { 273 var dt = getprop("/sim/time/delta-sec"); 274 var c = dt / (me.coeff + dt); 275 me.value = v * c + me.value * (1 - c); 276 }, 277}; 278 279 280 281# angular lowpass 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. 285# 286var angular_lowpass = { 287 new: func(coeff) { 288 var m = { parents: [angular_lowpass] }; 289 m.sin = lowpass.new(coeff); 290 m.cos = lowpass.new(coeff); 291 m.value = nil; 292 return m; 293 }, 294 filter: func(v) { 295 v *= D2R; 296 me.value = math.atan2(me.sin.filter(math.sin(v)), me.cos.filter(math.cos(v))) * R2D; 297 }, 298 set: func(v) { 299 v *= D2R; 300 me.sin.set(math.sin(v)); 301 me.cos.set(math.cos(v)); 302 }, 303 get: func { 304 me.value; 305 }, 306}; 307 308 309 310# data 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 315# by the system. 316# 317# SYNOPSIS: 318# data.add(<properties>); 319# data.save([<interval>]) 320# 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) 326# 327# SIGNALS: 328# /sim/signals/save ... set to 'true' right before saving. Can be used 329# to update values that are to be saved 330# 331# EXAMPLE: 332# var p = props.globals.getNode("/sim/model", 1); 333# var vec = [p, p]; 334# var hash = {"foo": p, "bar": p}; 335# 336# # add properties 337# aircraft.data.add("/sim/fg-root", p, "/sim/fg-home"); 338# aircraft.data.add(p, vec, hash, "/sim/fg-root"); 339# 340# # now save only once (and at exit/reinit, which is automatically done) 341# aircraft.data.save(); 342# 343# # or save now and every 30 sec (and at exit/reinit) 344# aircraft.data.save(0.5); 345# 346var data = { 347 init: func { 348 me.path = getprop("/sim/fg-home") ~ "/aircraft-data/" ~ getprop("/sim/aircraft") ~ ".xml"; 349 me.signalN = props.globals.getNode("/sim/signals/save", 1); 350 me.catalog = []; 351 me._timer = maketimer(60, me, me._save_); 352 353 setlistener("/sim/signals/reinit", func(n) { 354 if (n.getBoolValue() and getprop("/sim/startup/save-on-exit")) { 355 me._save_(); 356 } 357 }); 358 setlistener("/sim/signals/exit", func { 359 if (getprop("/sim/startup/save-on-exit")) { 360 me._save_(); 361 } 362 }); 363 }, 364 365 load: func { 366 if (io.stat(me.path) != nil) { 367 logprint(LOG_INFO, "loading aircraft data from ", me.path); 368 io.read_properties(me.path, props.globals); 369 } 370 }, 371 372 save: func(v = nil) { 373 if (v == nil) { 374 me._save_(); 375 me._timer.stop(); 376 } else { 377 me._timer.restart(60 * v); 378 } 379 }, 380 381 _save_: func { 382 size(me.catalog) or return; 383 logprint(LOG_INFO, "saving aircraft data to ", me.path); 384 me.signalN.setBoolValue(1); 385 var data = props.Node.new(); 386 foreach (var c; me.catalog) { 387 if (c[0] == `/`) { 388 c = substr(c, 1); 389 } 390 props.copy(props.globals.getNode(c, 1), data.getNode(c, 1)); 391 } 392 io.write_properties(me.path, data); 393 }, 394 395 add: func(p...) { 396 foreach (var n; props.nodeList(p)) { 397 append(me.catalog, n.getPath()); 398 } 399 }, 400}; 401 402 403 404# timer 405# ============================================================================== 406# class that implements timer that can be started, stopped, reset, and can 407# have its value saved to the aircraft specific data file. Saving the value 408# is done automatically by the aircraft.Data class. 409# 410# SYNOPSIS: 411# timer.new(<property> [, <resolution:double> [, <save:bool>]]) 412# 413# <property> ... property path or props.Node hash that holds the timer value 414# <resolution> ... timer update resolution -- interval in seconds in which the 415# timer property is updated while running (default: 1 s) 416# <save> ... bool that defines whether the timer value should be saved 417# and restored next time, as needed for Hobbs meters 418# (default: 1) 419# 420# EXAMPLES: 421# var hobbs_turbine = aircraft.timer.new("/sim/time/hobbs/turbine[0]", 60); 422# hobbs_turbine.start(); 423# 424# aircraft.timer.new("/sim/time/hobbs/battery", 60).start(); # anonymous timer 425# 426var timer = { 427 new: func(prop, res = 1, save = 1) { 428 var m = { parents: [timer] }; 429 m.node = makeNode(prop); 430 if (m.node.getType() == "NONE") 431 m.node.setDoubleValue(0); 432 433 me.systimeN = props.globals.getNode("/sim/time/elapsed-sec", 1); 434 m.last_systime = nil; 435 m.interval = res; 436 m.loopid = 0; 437 m.running = 0; 438 m.reinitL = setlistener("/sim/signals/reinit", func(n) { 439 if (n.getValue()) { 440 m.stop(); 441 m.total = m.node.getValue(); 442 } else { 443 m.node.setDoubleValue(m.total); 444 } 445 }); 446 if (save) { 447 data.add(m.node); 448 m.saveL = setlistener("/sim/signals/save", func m._save_()); 449 } else { 450 m.saveL = nil; 451 } 452 return m; 453 }, 454 del: func { 455 me.stop(); 456 removelistener(me.reinitL); 457 if (me.saveL != nil) 458 removelistener(me.saveL); 459 }, 460 start: func { 461 me.running and return; 462 me.last_systime = me.systimeN.getValue(); 463 if (me.interval != nil) 464 me._loop_(me.loopid); 465 me.running = 1; 466 me; 467 }, 468 stop: func { 469 me.running or return; 470 me.running = 0; 471 me.loopid += 1; 472 me._apply_(); 473 me; 474 }, 475 reset: func { 476 me.node.setDoubleValue(0); 477 me.last_systime = me.systimeN.getValue(); 478 }, 479 _apply_: func { 480 var sys = me.systimeN.getValue(); 481 me.node.setDoubleValue(me.node.getValue() + sys - me.last_systime); 482 me.last_systime = sys; 483 }, 484 _save_: func { 485 if (me.running) 486 me._apply_(); 487 }, 488 _loop_: func(id) { 489 id != me.loopid and return; 490 me._apply_(); 491 settimer(func me._loop_(id), me.interval); 492 }, 493}; 494 495 496 497# livery 498# ============================================================================= 499# Class that maintains livery XML files (see English Electric Lightning for an 500# example). The last used livery is saved on exit and restored next time. Livery 501# files are regular PropertyList XML files whose properties are copied to the 502# main tree. 503# 504# SYNOPSIS: 505# livery.init(<livery-dir> [, <name-path> [, <sort-path>]]); 506# 507# <livery-dir> ... directory with livery XML files, relative to $FG_ROOT 508# <name-path> ... property path to the livery name in the livery files 509# and the property tree (default: sim/model/livery/name) 510# <sort-path> ... property path to the sort criterion (default: same as 511# <name-path> -- that is: alphabetic sorting) 512# 513# EXAMPLE: 514# aircraft.livery.init("Aircraft/Lightning/Models/Liveries", 515# "sim/model/livery/variant", 516# "sim/model/livery/index"); # optional 517# 518# aircraft.livery.dialog.toggle(); 519# aircraft.livery.select("OEBH"); 520# aircraft.livery.next(); 521# 522var livery = { 523 init: func(dir, nameprop = "sim/model/livery/name", sortprop = nil) { 524 me.parents = [gui.OverlaySelector.new("Select Livery", dir, nameprop, 525 sortprop, "sim/model/livery/file")]; 526 me.dialog = me.parents[0]; 527 }, 528}; 529 530 531 532# livery_update 533# ============================================================================= 534# Class for maintaining liveries in MP aircraft. It is used in Nasal code that's 535# embedded in aircraft animation XML files, and checks in intervals whether the 536# parent aircraft has changed livery, in which case it changes the livery 537# in the remote aircraft accordingly. This class is a wrapper for overlay_update. 538# 539# SYNOPSIS: 540# livery_update.new(<livery-dir> [, <interval:10> [, <func>]]); 541# 542# <livery-dir> ... directory with livery files, relative to $FG_ROOT 543# <interval> ... checking interval in seconds (default: 10) 544# <func> ... callback function that's called with the ./sim/model/livery/file 545# contents as argument whenever the livery has changed. This can 546# be used for post-processing. 547# 548# EXAMPLE: 549# <nasal> 550# <load> 551# var livery_update = aircraft.livery_update.new( 552# "Aircraft/R22/Models/Liveries", 30, 553# func print("R22 livery update")); 554# </load> 555# 556# <unload> 557# livery_update.stop(); 558# </unload> 559# </nasal> 560# 561var livery_update = { 562 new: func(liveriesdir, interval = 10.01, callback = nil) { 563 var m = { parents: [livery_update, overlay_update.new()] }; 564 m.parents[1].add(liveriesdir, "sim/model/livery/file", callback); 565 m.parents[1].interval = interval; 566 return m; 567 }, 568 stop: func { 569 me.parents[1].stop(); 570 }, 571}; 572 573 574 575# overlay_update 576# ============================================================================= 577# Class for maintaining overlays in MP aircraft. It is used in Nasal code that's 578# embedded in aircraft animation XML files, and checks in intervals whether the 579# parent aircraft has changed an overlay, in which case it copies the respective 580# overlay to the aircraft's root directory. 581# 582# SYNOPSIS: 583# livery_update.new(); 584# livery_update.add(<overlay-dir>, <property> [, <callback>]); 585# 586# <overlay-dir> ... directory with overlay files, relative to $FG_ROOT 587# <property> ... MP property where the overlay file name can be found 588# (usually one of the sim/multiplay/generic/string properties) 589# <callback> ... callback function that's called with two arguments: 590# the file name (without extension) and the overlay directory 591# 592# EXAMPLE: 593# <nasal> 594# <load> 595# var update = aircraft.overlay_update.new(); 596# update.add("Aircraft/F4U/Models/Logos", "sim/multiplay/generic/string"); 597# </load> 598# 599# <unload> 600# update.stop(); 601# </unload> 602# </nasal> 603# 604var overlay_update = { 605 new: func { 606 var m = { parents: [overlay_update] }; 607 m.root = cmdarg(); 608 m.data = {}; 609 m.interval = 10.01; 610 if (m.root.getName() == "multiplayer") 611 m._loop_(); 612 return m; 613 }, 614 add: func(path, prop, callback = nil) { 615 var path = path ~ '/'; 616 me.data[path] = [me.root.initNode(prop, ""), "", 617 isfunc(callback) ? callback : func nil]; 618 return me; 619 }, 620 stop: func { 621 me._loop_ = func nil; 622 }, 623 _loop_: func { 624 foreach (var path; keys(me.data)) { 625 var v = me.data[path]; 626 var file = v[0].getValue(); 627 if (file != v[1]) { 628 io.read_properties(path ~ file ~ ".xml", me.root); 629 v[2](v[1] = file, path); 630 } 631 } 632 settimer(func me._loop_(), me.interval); 633 }, 634}; 635 636 637 638# steering 639# ============================================================================= 640# Class that implements differential braking depending on rudder position. 641# Note that this overrides the controls.applyBrakes() wrapper. If you need 642# your own version, then override it again after the steering.init() call. 643# 644# SYNOPSIS: 645# steering.init([<property> [, <threshold>]]); 646# 647# <property> ... property path or props.Node hash that enables/disables 648# brake steering (usually bound to the js trigger button) 649# <threshold> ... defines range (+- threshold) around neutral rudder 650# position in which both brakes are applied 651# 652# EXAMPLES: 653# aircraft.steering.init("/controls/gear/steering", 0.2); 654# aircraft.steering.init(); 655# 656var steering = { 657 init: func(switch = "/controls/gear/brake-steering", threshold = 0.3) { 658 me.threshold = threshold; 659 me.switchN = makeNode(switch); 660 me.switchN.setBoolValue(me.switchN.getBoolValue()); 661 me.leftN = props.globals.getNode("/controls/gear/brake-left", 1); 662 me.rightN = props.globals.getNode("/controls/gear/brake-right", 1); 663 me.rudderN = props.globals.getNode("/controls/flight/rudder", 1); 664 me.loopid = 0; 665 666 controls.applyBrakes = func(v, w = 0) { 667 if (w < 0) 668 steering.leftN.setValue(v); 669 elsif (w > 0) 670 steering.rightN.setValue(v); 671 else 672 steering.switchN.setValue(v); 673 } 674 setlistener(me.switchN, func(n) { 675 me.loopid += 1; 676 if (n.getValue()) 677 me._loop_(me.loopid); 678 else 679 me.setbrakes(0, 0); 680 }, 1); 681 }, 682 _loop_: func(id) { 683 id == me.loopid or return; 684 var rudder = me.rudderN.getValue(); 685 if (rudder > me.threshold) 686 me.setbrakes(0, rudder); 687 elsif (rudder < -me.threshold) 688 me.setbrakes(-rudder, 0); 689 else 690 me.setbrakes(1, 1); 691 692 settimer(func me._loop_(id), 0); 693 }, 694 setbrakes: func(left, right) { 695 me.leftN.setDoubleValue(left); 696 me.rightN.setDoubleValue(right); 697 }, 698}; 699 700 701 702# autotrim 703# ============================================================================= 704# Singleton class that supports quick trimming and compensates for the lack 705# of resistance/force feedback in most joysticks. Normally the pilot trims such 706# that no real or artificially generated (by means of servo motors and spring 707# preloading) forces act on the stick/yoke and it is in a comfortable position. 708# This doesn't work well on computer joysticks. 709# 710# SYNOPSIS: 711# autotrim.start(); # on key/button press 712# autotrim.stop(); # on key/button release (mod-up) 713# 714# USAGE: 715# (1) move the stick such that the aircraft is in an orientation that 716# you want to trim for (forward flight, hover, ...) 717# (2) press autotrim button and keep it pressed 718# (3) move stick/yoke to neutral position (center) 719# (4) release autotrim button 720# 721var autotrim = { 722 init: func { 723 me.elevator = me.Trim.new("elevator"); 724 me.aileron = me.Trim.new("aileron"); 725 me.rudder = me.Trim.new("rudder"); 726 me.loopid = 0; 727 me.active = 0; 728 }, 729 start: func { 730 me.active and return; 731 me.active = 1; 732 me.elevator.start(); 733 me.aileron.start(); 734 me.rudder.start(); 735 me._loop_(me.loopid += 1); 736 }, 737 stop: func { 738 me.active or return; 739 me.active = 0; 740 me.loopid += 1; 741 me.update(); 742 }, 743 _loop_: func(id) { 744 id == me.loopid or return; 745 me.update(); 746 settimer(func me._loop_(id), 0); 747 }, 748 update: func { 749 me.elevator.update(); 750 me.aileron.update(); 751 me.rudder.update(); 752 }, 753 Trim: { 754 new: func(name) { 755 var m = { parents: [autotrim.Trim] }; 756 m.trimN = props.globals.getNode("/controls/flight/" ~ name ~ "-trim", 1); 757 m.ctrlN = props.globals.getNode("/controls/flight/" ~ name, 1); 758 return m; 759 }, 760 start: func { 761 me.last = me.ctrlN.getValue(); 762 }, 763 update: func { 764 var v = me.ctrlN.getValue(); 765 me.trimN.setDoubleValue(me.trimN.getValue() + me.last - v); 766 me.last = v; 767 }, 768 }, 769}; 770 771 772 773# tyresmoke 774# ============================================================================= 775# Provides a property which can be used to contol particles used to simulate tyre 776# smoke on landing. Weight on wheels, vertical speed, ground speed, ground friction 777# factor are taken into account. Tyre slip is simulated by low pass filters. 778# 779# Modifications to the model file are required. 780# 781# Generic XML particle files are available, but are not mandatory 782# (see Hawker Seahawk for an example). 783# 784# SYNOPSIS: 785# aircraft.tyresmoke.new(gear index [, auto = 0]) 786# gear index - the index of the gear to which the tyre smoke is attached 787# auto - enable automatic update (recommended). defaults to 0 for backward compatibility. 788# aircraft.tyresmoke.del() 789# destructor. 790# aircraft.tyresmoke.update() 791# Runs the update. Not required if automatic updates are enabled. 792# 793# EXAMPLE: 794# var tyresmoke_0 = aircraft.tyresmoke.new(0); 795# tyresmoke_0.update(); 796# 797# PARAMETERS: 798# 799# number: index of gear to be animated, i.e. "2" for /gear/gear[2]/... 800# 801# auto: 1 when tyresmoke should start on update loop. 0 when you're going 802# to call the update method from one of your own loops. 803# 804# diff_norm: value adjusting the necessary percental change of roll-speed 805# to trigger tyre smoke. Default value is 0.05. More realistic results can 806# be achieved with significantly higher values (i.e. use 0.8). 807# 808# check_vspeed: 1 when tyre smoke should only be triggered when vspeed is negative 809# (usually doesn't work for all gear, since vspeed=0.0 after the first gear touches 810# ground). Use 0 to make tyre smoke independent of vspeed. 811# Note: in reality, tyre smoke doesn't depend on vspeed, but only on acceleration 812# and friction. 813# 814 815var tyresmoke = { 816 new: func(number, auto = 0, diff_norm = 0.05, check_vspeed=1) { 817 var m = { parents: [tyresmoke] }; 818 m.vertical_speed = (!check_vspeed) ? nil : props.globals.initNode("velocities/vertical-speed-fps"); 819 m.diff_norm = diff_norm; 820 m.speed = props.globals.initNode("velocities/groundspeed-kt"); 821 m.rain = props.globals.initNode("environment/metar/rain-norm"); 822 823 var gear = props.globals.getNode("gear/gear[" ~ number ~ "]/"); 824 m.wow = gear.initNode("wow"); 825 m.tyresmoke = gear.initNode("tyre-smoke", 0, "BOOL"); 826 m.friction_factor = gear.initNode("ground-friction-factor", 1); 827 m.sprayspeed = gear.initNode("sprayspeed-ms"); 828 m.spray = gear.initNode("spray", 0, "BOOL"); 829 m.spraydensity = gear.initNode("spray-density", 0, "DOUBLE"); 830 m.auto = auto; 831 m.listener = nil; 832 833 if (getprop("sim/flight-model") == "jsb") { 834 var wheel_speed = "fdm/jsbsim/gear/unit[" ~ number ~ "]/wheel-speed-fps"; 835 m.rollspeed = props.globals.initNode(wheel_speed); 836 m.get_rollspeed = func m.rollspeed.getValue() * 0.3043; 837 } else { 838 m.rollspeed = gear.initNode("rollspeed-ms"); 839 m.get_rollspeed = func m.rollspeed.getValue(); 840 } 841 842 m.lp = lowpass.new(2); 843 auto and m.update(); 844 return m; 845 }, 846 del: func { 847 if (me.listener != nil) { 848 removelistener(me.listener); 849 me.listener = nil; 850 } 851 me.auto = 0; 852 }, 853 update: func { 854 var rollspeed = me.get_rollspeed(); 855 var vert_speed = (me.vertical_speed) != nil ? me.vertical_speed.getValue() : -999; 856 var groundspeed = me.speed.getValue(); 857 var friction_factor = me.friction_factor.getValue(); 858 var wow = me.wow.getValue(); 859 var rain = me.rain.getValue(); 860 861 var filtered_rollspeed = me.lp.filter(rollspeed); 862 var diff = math.abs(rollspeed - filtered_rollspeed); 863 var diff_norm = diff > 0 ? diff / rollspeed : 0; 864 865 if (wow and vert_speed < -1.2 866 and diff_norm > me.diff_norm 867 and friction_factor > 0.7 and groundspeed > 50 868 and rain < 0.20) { 869 me.tyresmoke.setValue(1); 870 me.spray.setValue(0); 871 me.spraydensity.setValue(0); 872 } elsif (wow and groundspeed > 5 and rain >= 0.20) { 873 me.tyresmoke.setValue(0); 874 me.spray.setValue(1); 875 me.sprayspeed.setValue(rollspeed * 6); 876 me.spraydensity.setValue(rain * groundspeed); 877 } else { 878 me.tyresmoke.setValue(0); 879 me.spray.setValue(0); 880 me.sprayspeed.setValue(0); 881 me.spraydensity.setValue(0); 882 } 883 if (me.auto) { 884 if (wow) { 885 settimer(func me.update(), 0); 886 if (me.listener != nil) { 887 removelistener(me.listener); 888 me.listener = nil; 889 } 890 } elsif (me.listener == nil) { 891 me.listener = setlistener(me.wow, func me._wowchanged_(), 0, 0); 892 } 893 } 894 }, 895 _wowchanged_: func() { 896 if (me.wow.getValue()) { 897 me.lp.set(0); 898 me.update(); 899 } 900 }, 901}; 902 903# tyresmoke_system 904# ============================================================================= 905# Helper class to contain the tyresmoke objects for all the gears. 906# Will update automatically, nothing else needs to be done by the caller. 907# 908# SYNOPSIS: 909# aircraft.tyresmoke_system.new(<gear index 1>, <gear index 2>, ...) 910# <gear index> - the index of the gear to which the tyre smoke is attached 911# aircraft.tyresmoke_system.del() 912# destructor 913# EXAMPLE: 914# var tyresmoke_system = aircraft.tyresmoke_system.new(0, 1, 2, 3, 4); 915 916var tyresmoke_system = { 917 new: func { 918 var m = { parents: [tyresmoke_system] }; 919 # preset array to proper size 920 m.gears = []; 921 setsize(m.gears, size(arg)); 922 for(var i = size(arg) - 1; i >= 0; i -= 1) { 923 m.gears[i] = tyresmoke.new(arg[i], 1); 924 } 925 return m; 926 }, 927 del: func { 928 foreach(var gear; me.gears) { 929 gear.del(); 930 } 931 } 932}; 933 934# rain 935# ============================================================================= 936# Provides a property which can be used to control rain. Can be used to turn 937# off rain in internal views, and or used with a texture on canopies etc. 938# The output is co-ordinated with system precipitation: 939# 940# /sim/model/rain/raining-norm rain intensity 941# /sim/model/rain/flow-mps drop flow speed [m/s] 942# 943# See Hawker Seahawk for an example. 944# 945# SYNOPSIS: 946# aircraft.rain.init(); 947# aircraft.rain.update(); 948# 949var rain = { 950 init: func { 951 me.elapsed_timeN = props.globals.getNode("sim/time/elapsed-sec"); 952 me.dtN = props.globals.getNode("sim/time/delta-sec"); 953 954 me.enableN = props.globals.initNode("sim/rendering/precipitation-aircraft-enable", 0, "BOOL"); 955 me.precip_levelN = props.globals.initNode("environment/params/precipitation-level-ft", 0); 956 me.altitudeN = props.globals.initNode("position/altitude-ft", 0); 957 me.iasN = props.globals.initNode("velocities/airspeed-kt", 0); 958 me.rainingN = props.globals.initNode("sim/model/rain/raining-norm", 0); 959 me.flowN = props.globals.initNode("sim/model/rain/flow-mps", 0); 960 961 var canopyN = props.globals.initNode("gear/canopy/position-norm", 0); 962 var thresholdN = props.globals.initNode("sim/model/rain/flow-threshold-kt", 15); 963 964 setlistener(canopyN, func(n) me.canopy = n.getValue(), 1, 0); 965 setlistener(thresholdN, func(n) me.threshold = n.getValue(), 1); 966 setlistener("sim/rendering/precipitation-gui-enable", func(n) me.enabled = n.getValue(), 1); 967 setlistener("environment/metar/rain-norm", func(n) me.rain = n.getValue(), 1); 968 setlistener("sim/current-view/internal", func(n) me.internal = n.getValue(), 1); 969 }, 970 update: func { 971 var altitude = me.altitudeN.getValue(); 972 var precip_level = me.precip_levelN.getValue(); 973 974 if (me.enabled and me.internal and altitude < precip_level and me.canopy < 0.001) { 975 var time = me.elapsed_timeN.getValue(); 976 var ias = me.iasN.getValue(); 977 var dt = me.dtN.getValue(); 978 979 me.flowN.setDoubleValue(ias < me.threshold ? 0 : time * 0.5 + ias * NM2M * dt / 3600); 980 me.rainingN.setDoubleValue(me.rain); 981 if (me.enableN.getBoolValue()) 982 me.enableN.setBoolValue(0); 983 } else { 984 me.flowN.setDoubleValue(0); 985 me.rainingN.setDoubleValue(0); 986 if (me.enableN.getBoolValue() != 1) 987 me.enableN.setBoolValue(1); 988 } 989 }, 990}; 991 992 993 994# teleport 995# ============================================================================= 996# Usage: aircraft.teleport(lat:48.3, lon:32.4, alt:5000); 997# 998var teleport = func(airport = "", runway = "", lat = -9999, lon = -9999, alt = 0, 999 speed = 0, distance = 0, azimuth = 0, glideslope = 0, heading = 9999) { 1000 setprop("/sim/presets/airport-id", airport); 1001 setprop("/sim/presets/runway", runway); 1002 setprop("/sim/presets/parkpos", ""); 1003 setprop("/sim/presets/latitude-deg", lat); 1004 setprop("/sim/presets/longitude-deg", lon); 1005 setprop("/sim/presets/altitude-ft", alt); 1006 setprop("/sim/presets/airspeed-kt", speed); 1007 setprop("/sim/presets/offset-distance-nm", distance); 1008 setprop("/sim/presets/offset-azimuth-deg", azimuth); 1009 setprop("/sim/presets/glideslope-deg", glideslope); 1010 setprop("/sim/presets/heading-deg", heading); 1011 fgcommand("reposition"); 1012} 1013 1014 1015 1016# returns wind speed [kt] from given direction [deg]; useful for head-wind 1017# 1018var wind_speed_from = func(azimuth) { 1019 var dir = (getprop("/environment/wind-from-heading-deg") - azimuth) * D2R; 1020 return getprop("/environment/wind-speed-kt") * math.cos(dir); 1021} 1022 1023 1024 1025# returns true airspeed for given indicated airspeed [kt] and altitude [m] 1026# 1027var kias_to_ktas = func(kias, altitude) { 1028 var seapress = getprop("/environment/pressure-sea-level-inhg"); 1029 var seatemp = getprop("/environment/temperature-sea-level-degc"); 1030 var coralt_ft = altitude * M2FT + (29.92 - seapress) * 910; 1031 return kias * (1 + 0.00232848233 * (seatemp - 15)) 1032 * (1.0025 + coralt_ft * (0.0000153 1033 - kias * (coralt_ft * 0.0000000000003 + 0.0000000045) 1034 + (0.0000119 * (math.exp(coralt_ft * 0.000016) - 1)))); 1035} 1036 1037 1038 1039# HUD control class to handle both HUD implementations 1040# ============================================================================== 1041# 1042var HUD = { 1043 init: func { 1044 me.vis1N = props.globals.getNode("/sim/hud/visibility[1]", 1); 1045 me.currcolN = props.globals.getNode("/sim/hud/current-color", 1); 1046 me.currentPathN = props.globals.getNode("/sim/hud/current-path", 1); 1047 me.hudN = props.globals.getNode("/sim/hud", 1); 1048 me.paletteN = props.globals.getNode("/sim/hud/palette", 1); 1049 me.brightnessN = props.globals.getNode("/sim/hud/color/brightness", 1); 1050 me.currentN = me.vis1N; 1051 1052 # keep compatibility with earlier version of FG - hud/path[1] is 1053 # the default Hud 1054 me.currentPathN.setIntValue(1); 1055 }, 1056 cycle_color: func { # h-key 1057 if (!me.currentN.getBoolValue()) # if off, turn on 1058 return me.currentN.setBoolValue(1); 1059 1060 var i = me.currcolN.getValue() + 1; # if through, turn off 1061 if (i < 0 or i >= size(me.paletteN.getChildren("color"))) { 1062 me.currentN.setBoolValue(0); 1063 me.currcolN.setIntValue(0); 1064 } else { # otherwise change color 1065 me.currentN.setBoolValue(1); 1066 me.currcolN.setIntValue(i); 1067 } 1068 }, 1069 cycle_brightness: func { # H-key 1070 me.is_active() or return; 1071 var br = me.brightnessN.getValue() - 0.2; 1072 me.brightnessN.setValue(br > 0.01 ? br : 1); 1073 }, 1074 normal_type: func { # i-key 1075 me.currentPathN.setIntValue(1); 1076 }, 1077 cycle_type: func { # I-key 1078 var i = me.currentPathN.getValue() + 1; 1079 if (i < 1 or i > size(me.hudN.getChildren("path"))) { 1080 # back to the start 1081 me.currentPathN.setIntValue(1); 1082 } else { 1083 me.currentPathN.setIntValue(i); 1084 } 1085 }, 1086 is_active: func { 1087 me.vis1N.getValue(); 1088 }, 1089}; 1090 1091# crossfeed_valve 1092# ============================================================================= 1093# class that creates a fuel tank cross-feed valve. Designed for YASim aircraft; 1094# JSBSim aircraft can simply use systems code within the FDM (see 747-400 for 1095# an example). 1096# 1097# WARNING: this class requires the tank properties to be ready, so call new() 1098# after the FDM is initialized. 1099# 1100# SYNOPSIS: 1101# crossfeed_valve.new(<max_flow_rate>, <property>, <tank>, <tank>, ... ); 1102# crossfeed_valve.open(<update>); 1103# crossfeed_valve.close(<update>); 1104# 1105# <max_flow_rate> ... maximum transfer rate between the tanks in lbs/sec 1106# <property> ... property path to use as switch - pass nil to use no such switch 1107# <tank> ... number of a tank to connect - can have unlimited number of tanks connected 1108# <update> ... update switch property when opening/closing valve via Nasal - 0 or 1; by default, 1 1109# 1110# 1111# EXAMPLES: 1112# aircraft.crossfeed_valve.new(0.5, "/controls/fuel/x-feed", 0, 1, 2); 1113#------- 1114# var xfeed = aircraft.crossfeed_valve.new(1, nil, 0, 1); 1115# xfeed.open(); 1116# 1117var crossfeed_valve = { 1118 new: func(flow_rate, path) { 1119 var m = { parents: [crossfeed_valve] }; 1120 m.valve_open = 0; 1121 m.interval = 0.5; 1122 m.loopid = -1; 1123 m.flow_rate = flow_rate; 1124 if (path != nil) { 1125 m.switch_node = props.globals.initNode(path, 0, "BOOL"); 1126 setlistener(path, func(node) { 1127 if (node.getBoolValue()) m.open(0); 1128 else m.close(0); 1129 }, 1, 0); 1130 } 1131 m.tanks = []; 1132 for (var i = 0; i < size(arg); i += 1) { 1133 var tank = props.globals.getNode("consumables/fuel/tank[" ~ arg[i] ~ "]"); 1134 if (tank.getChild("level-lbs") != nil) append(m.tanks, tank); 1135 } 1136 return m; 1137 }, 1138 open: func(update_prop = 1) { 1139 if (me.valve_open == 1) return; 1140 if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(1); 1141 me.valve_open = 1; 1142 me.loopid += 1; 1143 settimer(func me._loop_(me.loopid), me.interval); 1144 }, 1145 close: func(update_prop = 1) { 1146 if (update_prop and contains(me, "switch_node")) me.switch_node.setBoolValue(0); 1147 me.valve_open = 0; 1148 }, 1149 _loop_: func(id) { 1150 if (id != me.loopid) return; 1151 var average_level = 0; 1152 var count = size(me.tanks); 1153 for (var i = 0; i < count; i += 1) { 1154 var level_node = me.tanks[i].getChild("level-lbs"); 1155 average_level += level_node.getValue(); 1156 } 1157 average_level /= size(me.tanks); 1158 var highest_diff = 0; 1159 for (var i = 0; i < count; i += 1) { 1160 var level = me.tanks[i].getChild("level-lbs").getValue(); 1161 var diff = math.abs(average_level - level); 1162 if (diff > highest_diff) highest_diff = diff; 1163 } 1164 for (var i = 0; i < count; i += 1) { 1165 var level_node = me.tanks[i].getChild("level-lbs"); 1166 var capacity = me.tanks[i].getChild("capacity-gal_us").getValue() * me.tanks[i].getChild("density-ppg").getValue(); 1167 var diff = math.abs(average_level - level_node.getValue()); 1168 var min_level = math.max(0, level_node.getValue() - me.flow_rate * diff / highest_diff); 1169 var max_level = math.min(capacity, level_node.getValue() + me.flow_rate * diff / highest_diff); 1170 var level = level_node.getValue() > average_level ? math.max(min_level, average_level) : math.min(max_level, average_level); 1171 level_node.setValue(level); 1172 } 1173 if (me.valve_open) settimer(func me._loop_(id), me.interval); 1174 } 1175}; 1176 1177props.globals.initNode("/sim/time/elapsed-sec", 0); 1178props.globals.initNode("/sim/time/delta-sec", 0); 1179props.globals.initNode("/sim/time/delta-realtime-sec", 0.00000001); 1180 1181HUD.init(); 1182data.init(); 1183autotrim.init(); 1184 1185##### temporary hack to provide backward compatibility for /sim/auto-coordination 1186##### remove this code when all references to /sim/auto-coordination are gone 1187var ac = props.globals.getNode("/sim/auto-coordination"); 1188if (ac != nil) { 1189 logprint(LOG_ALERT, "WARNING: using deprecated property "~ 1190 "/sim/auto-coordination. Please change to /controls/flight/auto-coordination"); 1191 ac.alias(props.globals.getNode("/controls/flight/auto-coordination", 1)); 1192} 1193#### end of temporary hack for /sim/auto-coordination 1194 1195if (!getprop("/sim/startup/restore-defaults")) { 1196 # load user-specific aircraft settings 1197 data.load(); 1198 var n = props.globals.getNode("/sim/aircraft-data"); 1199 if (n != nil) { 1200 foreach (var c; n.getChildren("path")) { 1201 if (c.getType() != "NONE") 1202 data.add(c.getValue()); 1203 } 1204 } 1205} 1206