diff --git a/Aircraft/Generic/Systems/Tests/FailureMgr/test_all.nas b/Aircraft/Generic/Systems/Tests/FailureMgr/test_all.nas new file mode 100644 index 000000000..bcb22be47 --- /dev/null +++ b/Aircraft/Generic/Systems/Tests/FailureMgr/test_all.nas @@ -0,0 +1,7 @@ +# Aggregation of all tests for the Failure Manager + +io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_cycle_counter.nas"); +io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_altitude_trigger.nas"); +io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_mcbf_trigger.nas"); +io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_mtbf_trigger.nas"); +io.include("Aircraft/Generic/Systems/Tests/FailureMgr/test_failure_mode.nas"); diff --git a/Aircraft/Generic/Systems/Tests/FailureMgr/test_altitude_trigger.nas b/Aircraft/Generic/Systems/Tests/FailureMgr/test_altitude_trigger.nas new file mode 100644 index 000000000..f7cf9b0f3 --- /dev/null +++ b/Aircraft/Generic/Systems/Tests/FailureMgr/test_altitude_trigger.nas @@ -0,0 +1,155 @@ +# AltitudeTrigger unit tests +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +io.include("Aircraft/Generic/Systems/Tests/test.nas"); +io.include("Aircraft/Generic/Systems/failures.nas"); + +var TestAltitudeTrigger = { + + parents: [TestSuite], + + setup: func { + props.globals.initNode("/test"); + }, + + cleanup: func { + me.trigger = nil; + props.globals.getNode("/test").remove(); + }, + + test_binding: func { + + setprop("/test/foreign-property", 25); + + me.trigger = AltitudeTrigger.new(100, 200); + me.trigger.bind("/test/"); + + assert_prop_exists("/test/reset"); + assert_prop_exists("/test/min-altitude-ft"); + assert_prop_exists("/test/min-altitude-ft"); + + me.trigger.unbind(); + + fail_if_prop_exists("/test/reset"); + fail_if_prop_exists("/test/min-altitude-ft"); + fail_if_prop_exists("/test/min-altitude-ft"); + + assert_prop_exists("/test/foreign-property"); + }, + + test_props_are_read_on_reset: func { + + me.trigger = AltitudeTrigger.new(100, 200); + me.trigger.bind("/test/"); + + assert(me.trigger.params["min-altitude-ft"] == 100); + assert(me.trigger.params["max-altitude-ft"] == 200); + + setprop("/test/min-altitude-ft", 1000); + setprop("/test/max-altitude-ft", 2000); + + assert(me.trigger.params["min-altitude-ft"] == 100); + assert(me.trigger.params["max-altitude-ft"] == 200); + + me.trigger.reset(); + + assert(me.trigger.params["min-altitude-ft"] == 1000); + assert(me.trigger.params["max-altitude-ft"] == 2000); + }, + + test_trigger_fires_within_min_and_max: func { + + me.trigger = AltitudeTrigger.new(100, 200); + + me.trigger._altitude_prop = "/test/fake-altitude-ft"; + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 0); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 300); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 150); + assert(me.trigger.update() == 1); + assert(me.trigger.fired); + }, + + test_trigger_accepts_nil_max: func { + + me.trigger = AltitudeTrigger.new(500, nil); + me.trigger._altitude_prop = "/test/fake-altitude-ft"; + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", -250); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 0); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 250); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 750); + assert(me.trigger.update() == 1); + assert(me.trigger.fired); + }, + + test_trigger_accepts_nil_min: func { + + me.trigger = AltitudeTrigger.new(nil, 500); + me.trigger._altitude_prop = "/test/fake-altitude-ft"; + me.trigger.bind("/test/trigger/"); + + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 750); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 500); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-altitude-ft", 250); + assert(me.trigger.update() == 1); + assert(me.trigger.fired); + + me.trigger.reset(); + + setprop("/test/fake-altitude-ft", -250); + assert(me.trigger.update() == 1); + assert(me.trigger.fired); + }, + + test_trigger_dies_if_both_params_are_nil: func { + call(AltitudeTrigger.new, [nil, nil], AltitudeTrigger, var err = []); + assert(size(err) > 0); + }, + + test_to_str: func { + me.trigger = AltitudeTrigger.new(100, 200); + call(me.trigger.to_str, [], me.trigger, var err = []); + assert(size(err) == 0); + } +}; diff --git a/Aircraft/Generic/Systems/Tests/FailureMgr/test_cycle_counter.nas b/Aircraft/Generic/Systems/Tests/FailureMgr/test_cycle_counter.nas new file mode 100644 index 000000000..30c0e4a8e --- /dev/null +++ b/Aircraft/Generic/Systems/Tests/FailureMgr/test_cycle_counter.nas @@ -0,0 +1,124 @@ +# CycleCounter unit tests +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +io.include("Aircraft/Generic/Systems/Tests/test.nas"); +io.include("Aircraft/Generic/Systems/failures.nas"); + +var TestCycleCounter = { + + parents: [TestSuite], + + setup: func { + props.globals.initNode("/test"); + }, + + cleanup: func { + props.globals.getNode("/test").remove(); + me.counter = nil; + }, + + _shake_that_prop: func (pattern=nil) { + + if (pattern == nil) + pattern = [0, -10, 10, -10, 10, -10, 10, 0]; + + setprop("/test/property", pattern[0]); + me.counter.reset(); + + var i = 0; + var value = pattern[0]; + var target = pattern[1]; + var delta = 0; + + while(i < size(pattern) - 1) { + + target = pattern[i+1]; + delta = pattern[i+1] > pattern[i] ? 1 : -1; + + while(value != target) { + value += delta; + setprop("/test/property", value); + } + + i += 1; + } + }, + + test_cycles_dont_grow_while_disabled: func { + me.counter = CycleCounter.new("/test/property"); + me._shake_that_prop(); + assert(me.counter.cycles == 0); + }, + + test_cycles_grow_while_enabled: func { + me.counter = CycleCounter.new("/test/property"); + + me._shake_that_prop(); + assert(me.counter.cycles == 0); + + me.counter.enable(); + + me._shake_that_prop(); + assert(me.counter.cycles == 3); + }, + + test_reset: func { + me.counter = CycleCounter.new("/test/property"); + me.counter.enable(); + + me._shake_that_prop(); + assert(me.counter.cycles > 0); + + me.counter.reset(); + assert(me.counter.cycles == 0); + }, + + test_callback_every_half_cycle: func { + var count = 0; + + me.counter = CycleCounter.new( + property: "/test/property", + on_update: func (cycles) { count += 1 }); + + me.counter.enable(); + me._shake_that_prop(); + + assert(count == 6); + }, + + test_callback_reports_cycle_count: func { + var count = 0; + var cb = func (cycles) { + count += 1; + assert(cycles == count * 0.5); + }; + + me.counter = CycleCounter.new( + property: "/test/property", on_update: cb); + + me.counter.enable(); + me._shake_that_prop(); + }, + + test_counter_works_for_binary_props: func { + me.counter = CycleCounter.new("/test/property"); + me.counter.enable(); + me._shake_that_prop([0, 1, 0, 1, 0, 1]); + assert(me.counter.cycles == 2); + } +}; diff --git a/Aircraft/Generic/Systems/Tests/FailureMgr/test_failure_mode.nas b/Aircraft/Generic/Systems/Tests/FailureMgr/test_failure_mode.nas new file mode 100644 index 000000000..d6fd9bd71 --- /dev/null +++ b/Aircraft/Generic/Systems/Tests/FailureMgr/test_failure_mode.nas @@ -0,0 +1,113 @@ +# AltitudeTrigger unit tests +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +io.include("Aircraft/Generic/Systems/Tests/test.nas"); +io.include("Aircraft/Generic/Systems/failures.nas"); + +var TestFailureMode = { + + parents: [TestSuite], + + setup: func { + props.globals.initNode("/test"); + }, + + cleanup: func { + me.mode = nil; + props.globals.getNode("/test").remove(); + }, + + test_binding: func { + var actuator = { parents: [FailureMgr.FailureActuator] }; + setprop("/test/foreign-property", 25); + + me.mode = FailureMgr.FailureMode.new( + id: "instruments/compass", + description: "a description", + actuator: actuator); + + me.mode.bind("/test/"); + assert_prop_exists("/test/instruments/compass/failure-level"); + + me.mode.unbind(); + fail_if_prop_exists("/test/instruments/compass/failure-level"); + fail_if_prop_exists("/test/instruments/compass"); + assert_prop_exists("/test/foreign-property"); + }, + + test_set_failure_level_calls_actuator: func { + var level = 0; + var actuator = { + parents: [FailureMgr.FailureActuator], + set_failure_level: func (l) { level = l }, + }; + + me.mode = FailureMgr.FailureMode.new( + id: "instruments/compass", + description: "a description", + actuator: actuator); + me.mode.bind("/test/"); + + me.mode.set_failure_level(1); + assert(level == 1); + }, + + test_actuator_gets_called_from_prop: func { + var level = 0; + var actuator = { + parents: [FailureMgr.FailureActuator], + set_failure_level: func (l) { level = l }, + }; + + me.mode = FailureMgr.FailureMode.new( + id: "instruments/compass", + description: "a description", + actuator: actuator); + + me.mode.bind("/test/"); + setprop("/test/instruments/compass/failure-level", 1); + assert(level == 1); + }, + + test_setting_level_from_nasal_is_shown_in_prop: func { + var level = 0; + var actuator = { + parents: [FailureMgr.FailureActuator], + set_failure_level: func (l) { level = l }, + }; + + me.mode = FailureMgr.FailureMode.new( + id: "instruments/compass", + description: "a description", + actuator: actuator); + + me.mode.bind("/test/"); + + me.mode.set_failure_level(1); + assert(level == 1); + + var prop_value = getprop("/test/instruments/compass/failure-level"); + assert(prop_value == 1); + + me.mode.set_failure_level(0.5); + assert(level == 0.5); + + prop_value = getprop("/test/instruments/compass/failure-level"); + assert(prop_value == 0.5); + } +}; diff --git a/Aircraft/Generic/Systems/Tests/FailureMgr/test_mcbf_trigger.nas b/Aircraft/Generic/Systems/Tests/FailureMgr/test_mcbf_trigger.nas new file mode 100644 index 000000000..14a7d511a --- /dev/null +++ b/Aircraft/Generic/Systems/Tests/FailureMgr/test_mcbf_trigger.nas @@ -0,0 +1,116 @@ +# McbfTrigger unit tests +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +io.include("Aircraft/Generic/Systems/Tests/test.nas"); +io.include("Aircraft/Generic/Systems/failures.nas"); + +var TestMcbfTrigger = { + + parents: [TestSuite], + + setup: func { + props.globals.initNode("/test"); + }, + + cleanup: func { + me.trigger = nil; + props.globals.getNode("/test").remove(); + }, + + _do_one_cycle: func (prop) { + setprop(prop, 10); + setprop(prop, -10); + setprop(prop, 0); + }, + + test_binding: func { + setprop("/test/property", 0); + + me.trigger = McbfTrigger.new("/test/property", 3); + me.trigger.bind("/test/"); + + assert_prop_exists("/test/reset"); + assert_prop_exists("/test/mcbf"); + + me.trigger.unbind(); + fail_if_prop_exists("/test/reset"); + fail_if_prop_exists("/test/mcbf"); + assert_prop_exists("/test/property"); + }, + + test_trigger_fires_after_activation_cycles: func { + setprop("/test/property", 25); + me.trigger = McbfTrigger.new("/test/property", 3); + me.trigger.activation_cycles = 3; + me.trigger.enable(); + + assert(!me.trigger.fired); + + for (var i = 1; i < 5; i += 1) { + me._do_one_cycle("/test/property"); + assert(me.trigger.fired == (i > 3)); + } + }, + + test_trigger_notifies_observer_once: func { + var observer_called = 0; + var on_fire = func observer_called += 1; + + setprop("/test/property", 25); + me.trigger = McbfTrigger.new("/test/property", 3); + me.trigger.activation_cycles = 3; + me.trigger.on_fire = on_fire; + me.trigger.enable(); + + assert(!me.trigger.fired); + + for (var i = 1; i < 5; i += 1) + me._do_one_cycle("/test/property"); + + assert(observer_called == 1); + }, + + test_reset: func { + setprop("/test/property", 25); + me.trigger = McbfTrigger.new("/test/property", 3); + me.trigger.activation_cycles = 3; + me.trigger.bind("/test"); + me.trigger.enable(); + + for (var i = 1; i < 5; i += 1) + me._do_one_cycle("/test/property"); + + assert(me.trigger.fired); + + me.trigger.reset(); + me.trigger.activation_cycles = 3; + + assert(!me.trigger.fired); + + for (var i = 1; i < 5; i += 1) { + me._do_one_cycle("/test/property"); + assert(me.trigger.fired == (i > 3)); + } + }, + + test_to_str: func { + me.trigger = McbfTrigger.new("/test/property", 3); + call(me.trigger.to_str, [], me.trigger, var err = []); + assert(size(err) == 0); + } +}; diff --git a/Aircraft/Generic/Systems/Tests/FailureMgr/test_mtbf_trigger.nas b/Aircraft/Generic/Systems/Tests/FailureMgr/test_mtbf_trigger.nas new file mode 100644 index 000000000..dd54d2b81 --- /dev/null +++ b/Aircraft/Generic/Systems/Tests/FailureMgr/test_mtbf_trigger.nas @@ -0,0 +1,87 @@ +# MtbfTrigger unit tests +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +io.include("Aircraft/Generic/Systems/Tests/test.nas"); +io.include("Aircraft/Generic/Systems/failures.nas"); + +var TestMtbfTrigger = { + + parents: [TestSuite], + + setup: func { + props.globals.initNode("/test"); + }, + + cleanup: func { + me.trigger = nil; + props.globals.getNode("/test").remove(); + }, + + test_binding: func { + + setprop("/test/foreign-property", 25); + + me.trigger = MtbfTrigger.new(60); + me.trigger.bind("/test/"); + + assert_prop_exists("/test/reset"); + assert_prop_exists("/test/mtbf"); + + me.trigger.unbind(); + + fail_if_prop_exists("/test/reset"); + fail_if_prop_exists("/test/mtbf"); + + assert_prop_exists("/test/foreign-property"); + }, + + test_props_are_read_on_reset: func { + + me.trigger = MtbfTrigger.new(60); + me.trigger.bind("/test/"); + assert(me.trigger.params["mtbf"] == 60); + + setprop("/test/mtbf", 120); + assert(me.trigger.params["mtbf"] == 60); + + me.trigger.reset(); + assert(me.trigger.params["mtbf"] == 120); + }, + + test_trigger_fires_after_fire_time: func { + + me.trigger = MtbfTrigger.new(60); + me.trigger._time_prop = "/test/fake-time-sec"; + me.trigger.fire_time = 60; + assert(!me.trigger.fired); + + setprop("/test/fake-time-sec", 50); + assert(me.trigger.update() == 0); + assert(!me.trigger.fired); + + setprop("/test/fake-time-sec", 70); + assert(me.trigger.update() == 1); + assert(me.trigger.fired); + }, + + test_to_str: func { + me.trigger = MtbfTrigger.new(60); + call(me.trigger.to_str, [], me.trigger, var err = []); + assert(size(err) == 0); + } +}; diff --git a/Aircraft/Generic/Systems/Tests/test.nas b/Aircraft/Generic/Systems/Tests/test.nas new file mode 100644 index 000000000..0a13ca1e7 --- /dev/null +++ b/Aircraft/Generic/Systems/Tests/test.nas @@ -0,0 +1,139 @@ +# Minimalistic framework for automated testing in Nasal +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +# TestSuite +# +# Tests are organized in test suites. Each test suite contains an arbitrary +# number of tests, and two special methods "setup" and "cleanup". Setup is +# called before every test, and cleanup is called after every test. +# +# In order to define a test suite, you have to create an object parented to +# TestSuite. The testing framework will identify any method in your object whose +# name starts with "test_" as a test case to be run. +# +# Important: The order in which test cases and test suites are executed +# is undefined! +# +# Example: +# +# var MyTestSuite = { +# +# parents: [TestSuite], +# +# setup: func { +# Stuff to do before every test... +# This is optional. You don't need to provide it if you don't use it. +# }, +# +# cleanup: func { +# Stuff to do after every test... +# Also optional. +# }, +# +# my_auxiliary_function: func { +# Methods that do not start with "test_" will not be executed by the +# test runner. You can define as many auxiliary functions in the test +# suite as you wish. +# }, +# +# test_trivial_test: func { +# This is a real test (starts with "test_"), and it will be run by the +# framework when test_run() is called.# +# } +# }; + +var TestSuite = { + setup: func 0, + cleanup: func 0 +}; + +# run_tests([namespace]) +# +# Executes all test suites found in the given namespace. If no namespace is +# specified, then the namespace where run_tests is defined is used by default. +# +# An effective way to work with the framework is to just include the framework +# from your test files: +# +# io.include(".../test.nas"); +# +# and then execute a script like this in the Nasal Console: +# +# delete(globals, "test"); +# io.load_nasal(".../my_test_suite.nas", "test"); +# test.run_tests(); +# +# What this script does is: it empties the "test" namespace and then loads your +# script into that namespace. The test framework will be loaded in there as +# well if it was io.include'd in my_test_suite.nas. Finally, all test suites +# in the "test" namespace are executed. + +var run_tests = func(namespace=nil) { + + var ns = namespace != nil ? namespace : closure(run_tests, 1); + + var passed = 0; + var failed = 0; + var err = []; + + foreach(var suite_name; keys(ns)) { + var suite = ns[suite_name]; + + if (!isa(suite, TestSuite)) + continue; + + print("Running test suite ", suite_name); + + foreach (var test_name; keys(suite)) { + + if (find("test_", test_name) != 0) + continue; + + # Run the test case + setsize(err, 0); + contains(suite, "setup") and call(suite.setup, [], suite, err); + size(err) == 0 and call(suite[test_name], [], suite, err); + size(err) == 0 and contains(suite, "cleanup") and call(suite.cleanup, [], suite, err); + + if (size(err) == 0) { + passed += 1; + continue; + } + + failed += 1; + print("Test ", test_name, " FAILED\n"); + debug.printerror(err); + } + } + + print(sprintf("\n%d tests run. %d passed, %d failed", + passed + failed, passed, failed)); +} + + +var assert_prop_exists = func (prop) { + assert(props.globals.getNode(prop) != nil, + sprintf("Property %s does not exist", prop)); +} + + +var fail_if_prop_exists = func (prop) { + assert(props.globals.getNode(prop) == nil, + sprintf("Property %s exists", prop)); +} diff --git a/Aircraft/Generic/Systems/compat_failure_modes.nas b/Aircraft/Generic/Systems/compat_failure_modes.nas new file mode 100644 index 000000000..1fdfe5645 --- /dev/null +++ b/Aircraft/Generic/Systems/compat_failure_modes.nas @@ -0,0 +1,210 @@ +# Compatibility failure modes +# +# Loads FailureMgr with the failure modes that where previously hardcoded, +# emulating former behavior and allowing backward compatibility. +# +# Copyright (C) 2014 Anton Gomez Alvedro +# Based on previous work by Stuart Buchanan, Erobo & John Denker +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +io.include("Aircraft/Generic/Systems/failures.nas"); + + +MTBF = 0; +MCBF = 1; + +SERV = 0; +JAM = 1; +ENG = 2; + + +var compat_modes = [ + # Instruments + { id: "instrumentation/adf", type: MTBF, failure: SERV, desc: "ADF" }, + { id: "instrumentation/dme", type: MTBF, failure: SERV, desc: "DME" }, + { id: "instrumentation/airspeed-indicator", type: MTBF, failure: SERV, desc: "ASI" }, + { id: "instrumentation/altimeter", type: MTBF, failure: SERV, desc: "Altimeter" }, + { id: "instrumentation/attitude-indicator", type: MTBF, failure: SERV, desc: "Attitude Indicator" }, + { id: "instrumentation/heading-indicator", type: MTBF, failure: SERV, desc: "Heading Indicator" }, + { id: "instrumentation/magnetic-compass", type: MTBF, failure: SERV, desc: "Magnetic Compass" }, + { id: "instrumentation/nav/gs", type: MTBF, failure: SERV, desc: "Nav 1 Glideslope" }, + { id: "instrumentation/nav/cdi", type: MTBF, failure: SERV, desc: "Nav 1 CDI" }, + { id: "instrumentation/nav[1]/gs", type: MTBF, failure: SERV, desc: "Nav 2 Glideslope" }, + { id: "instrumentation/nav[1]/cdi", type: MTBF, failure: SERV, desc: "Nav 2 CDI" }, + { id: "instrumentation/slip-skid-ball", type: MTBF, failure: SERV, desc: "Slip/Skid Ball" }, + { id: "instrumentation/turn-indicator", type: MTBF, failure: SERV, desc: "Turn Indicator" }, + { id: "instrumentation/vertical-speed-indicator", type: MTBF, failure: SERV, desc: "VSI" }, + + # Systems + { id: "systems/electrical", type: MTBF, failure: SERV, desc: "Electrical system" }, + { id: "systems/pitot", type: MTBF, failure: SERV, desc: "Pitot system" }, + { id: "systems/static", type: MTBF, failure: SERV, desc: "Static system" }, + { id: "systems/vacuum", type: MTBF, failure: SERV, desc: "Vacuum system" }, + + # Controls + { id: "controls/flight/aileron", type: MTBF, failure: JAM, desc: "Aileron" }, + { id: "controls/flight/elevator", type: MTBF, failure: JAM, desc: "Elevator" }, + { id: "controls/flight/rudder", type: MTBF, failure: JAM, desc: "Rudder" }, + { id: "controls/flight/flaps", type: MCBF, failure: JAM, desc: "Flaps" }, + { id: "controls/flight/speedbrake", type: MCBF, failure: JAM, desc: "Speed Brake" }, + { id: "controls/gear", type: MCBF, failure: SERV, desc: "Gear", prop: "/gear", mcbf_prop: "/controls/gear/gear-down" } +]; + + +## +# Handles the old failures.nas property tree interface, +# sending the appropriate commands to the new FailureMgr. + +var compat_listener = func(prop) { + + var new_trigger = func { + if (name == "mtbf") { + MtbfTrigger.new(value); + } + else { + var control = id; + + forindex(var i; compat_modes) { + var mode = compat_modes[i]; + if (mode.id == id and contains(compat_modes[i], "mcbf_prop")) { + control = mode.mcbf_prop; + break; + } + } + + McbfTrigger.new(control, value); + } + }; + + var name = prop.getName(); + var value = prop.getValue(); + var id = string.replace(io.dirname(prop.getPath()), FailureMgr.proproot, ""); + id = string.trim(id, 0, func(c) c == `/`); + + if (name == "serviceable") { + FailureMgr.set_failure_level(id, 1 - value); + return; + } + + if (name == "failure-level") { + setprop(io.dirname(prop.getPath()) ~ "/serviceable", value ? 0 : 1); + return; + } + + # mtbf and mcbf parameter handling + var trigger = FailureMgr.get_trigger(id); + + if (value == 0) { + trigger != nil and FailureMgr.set_trigger(id, nil); + return; + } + + if (trigger == nil) { + FailureMgr.set_trigger(id, new_trigger()); + } + else { + trigger.set_param(name, value); + trigger.reset(); + } +} + +## +# Called from the ramdom-failures dialog to set the global MCBF parameter + +var apply_global_mcbf = func(value) { + foreach (var mode; compat_modes) { + mode.type != MCBF and continue; + setprop(FailureMgr.proproot ~ mode.id ~ "/mcbf", value); + } +} + +## +# Called from the ramdom-failures dialog to set the global MTBF parameter + +var apply_global_mtbf = func(value) { + foreach (var mode; compat_modes) { + mode.type != MTBF and continue; + setprop(FailureMgr.proproot ~ mode.id ~ "/mtbf", value); + } +} + +## +# Discover aircraft engines dynamically and add a failure mode to the +# compat_modes table for each engine. + +var populate_engine_data = func { + + var engines = props.globals.getNode("/engines"); + var engine_id = 0; + + foreach (var e; engines.getChildren("engine")) { + var starter = e.getChild("starter"); + var running = e.getChild("running"); + + (starter != nil and starter != "" and starter.getType() != "NONE") + or (running != nil and running != "" and running.getType() != "NONE") + or continue; + + var id = "engines/engine"; + if (engine_id > 0) + id = id ~ "[" ~ engine_id ~ "]"; + + var entry = { + id: id, + desc: "Engine " ~ (engine_id + 1), + type: MTBF, + failure: ENG + }; + + append(compat_modes, entry); + engine_id += 1; + } +} + +## +# Subscribes all failure modes that the old failures.nas module did, +# and recreates the same property tree interface (more or less). + +var compat_setup = func { + + removelistener(lsnr); + populate_engine_data(); + + foreach (var m; compat_modes) { + var control_prop = contains(m, "prop") ? m.prop : m.id; + + FailureMgr.add_failure_mode( + id: m.id, + description: m.desc, + actuator: if (m.failure == SERV) set_unserviceable(control_prop) + elsif (m.failure == JAM) set_readonly(control_prop) + else fail_engine(io.basename(control_prop))); + + # Recreate the prop tree interface + var prop = FailureMgr.proproot ~ m.id; + var n = props.globals.initNode(prop ~ "/serviceable", 1, "BOOL"); + + setlistener(n, compat_listener, 0, 0); + setlistener(prop ~ "/failure-level", compat_listener, 0, 0); + + var trigger_type = (m.type == MTBF) ? "/mtbf" : "/mcbf"; + setprop(prop ~ trigger_type, 0); + setlistener(prop ~ trigger_type, compat_listener, 0, 0); + } +} + + +var lsnr = setlistener("sim/signals/fdm-initialized", compat_setup); diff --git a/Aircraft/Generic/Systems/failures.nas b/Aircraft/Generic/Systems/failures.nas new file mode 100644 index 000000000..998539e9c --- /dev/null +++ b/Aircraft/Generic/Systems/failures.nas @@ -0,0 +1,365 @@ +# Failure simulation library +# +# Collection of generic Triggers and FailureActuators for programming the +# FailureMgr Nasal module. +# +# Copyright (C) 2014 Anton Gomez Alvedro +# Based on previous work by Stuart Buchanan, Erobo & John Denker +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +# +# Functions for generating FailureActuators +# ------------------------------------------ + +## +# Returns an actuator object that will set the serviceable property at +# the given node to zero when the level of failure is > 0. + +var set_unserviceable = func(path) { + + var prop = path ~ "/serviceable"; + + if (props.globals.getNode(prop) == nil) + props.globals.initNode(prop, 1, "BOOL"); + + return { + parents: [FailureMgr.FailureActuator], + set_failure_level: func(level) setprop(prop, level > 0 ? 0 : 1), + get_failure_level: func { getprop(prop) ? 0 : 1 } + } +} + +## +# Returns an actuator object that will make the given property read only. +# This prevents any other system from updating it, and effectively jamming +# whatever it is that is controlling. + +var set_readonly = func(property) { + return { + parents: [FailureMgr.FailureActuator], + + set_failure_level: func(level) { + var pnode = props.globals.getNode(property); + pnode.setAttribute("writable", level > 0 ? 0 : 1); + }, + + get_failure_level: func { + var pnode = props.globals.getNode(property); + pnode.getAttribute("writable") ? 0 : 1; + } + } +} + +## +# Returns an an actuator object the manipulates engine controls (magnetos & +# cutoff) to simulate an engine failure. Sets these properties to read only +# while the system is failed. + +var fail_engine = func(engine) { + return { + parents: [FailureMgr.FailureActuator], + level: 0, + magnetos: props.globals.getNode("/controls/engines/" ~ engine ~ "/magnetos", 1), + cutoff: props.globals.getNode("/controls/engines/" ~ engine ~ "/cutoff", 1), + + get_failure_level: func me.level, + + set_failure_level: func(level) { + if (level) { + # Switch off the engine, and disable writing to it. + me.magnetos.setValue(0); + me.magnetos.setAttribute("writable", 0); + me.cutoff.setValue(1); + me.cutoff.setAttribute("writable", 0); + } + else { + # Enable the properties, but don't set the magnetos, as they may + # be off for a reason. + me.magnetos.setAttribute("writable", 1); + me.cutoff.setAttribute("writable", 1); + me.cutoff.setValue(0); + } + me.level = level; + } + } +} + + +# +# Triggers +# --------- + +## +# Returns a random number from a Normal distribution with given mean and +# standard deviation. + +var norm_rand = func(mean, std) { + var r = -2 * math.ln(1 - rand()); + var a = 2 * math.pi * (1 - rand()); + return mean + (math.sqrt(r) * math.sin(a) * std); +}; + +## +# Trigger object that will fire when aircraft altitude is between +# min and max, both specified in feet. One of min or max may be nil for +# expressing "altitude > x" or "altitude < x" conditions. + +var AltitudeTrigger = { + + parents: [FailureMgr.Trigger], + requires_polling: 1, + + new: func(min, max) { + min != nil or max != nil or + die("AltitudeTrigger.new: either min or max must be specified"); + + var m = FailureMgr.Trigger.new(); + m.parents = [AltitudeTrigger]; + m.params["min-altitude-ft"] = min; + m.params["max-altitude-ft"] = max; + m._altitude_prop = "/position/altitude-ft"; + return m; + }, + + to_str: func { + # TODO: Handle min or max == nil + sprintf("Altitude between %d and %d ft", + int(me.params["min-altitude-ft"]), int(me.params["max-altitude-ft"])) + }, + + update: func { + var alt = getprop(me._altitude_prop); + + var min = me.params["min-altitude-ft"]; + var max = me.params["max-altitude-ft"]; + + me.fired = min != nil ? min < alt : 1; + me.fired = max != nil ? me.fired and alt < max : me.fired; + } +}; + +## +# Trigger object that fires when the aircraft's position is within a certain +# distance of a given waypoint. + +var WaypointTrigger = { + + parents: [FailureMgr.Trigger], + requires_polling: 1, + + new: func(lat, lon, distance) { + var wp = geo.Coord.new(); + wp.set_latlon(lat, lon); + + var m = FailureMgr.Trigger.new(); + m.parents = [WaypointTrigger]; + m.params["latitude-deg"] = lat; + m.params["longitude-deg"] = lon; + m.params["distance-nm"] = distance; + m.waypoint = wp; + return m; + }, + + reset: func { + call(FailureMgr.Trigger.reset, [], me); + me.waypoint.set_latlon(me.params["latitude-deg"], + me.params["longitude-deg"]); + }, + + to_str: func { + sprintf("Within %.2f miles of %s", me.params["distance-nm"], + geo.format(me.waypoint.lat, me.waypoint.lon)); + }, + + update: func { + var d = geo.aircraft_position().distance_to(me.waypoint) * M2NM; + me.fired = d < me.params["distance-nm"]; + } +}; + +## +# Trigger object that will fire on average after the specified time. + +var MtbfTrigger = { + + parents: [FailureMgr.Trigger], + # TODO: make this trigger async + requires_polling: 1, + + new: func(mtbf) { + var m = FailureMgr.Trigger.new(); + m.parents = [MtbfTrigger]; + m.params["mtbf"] = mtbf; + m.fire_time = 0; + m._time_prop = "/sim/time/elapsed-sec"; + return m; + }, + + reset: func { + call(FailureMgr.Trigger.reset, [], me); + # TODO: use an elapsed time prop that accounts for speed-up and pause + me.fire_time = getprop(me._time_prop) + + norm_rand(me.params["mtbf"], me.params["mtbf"] / 10); + }, + + to_str: func { + sprintf("Mean time between failures: %f.1 mins", me.params["mtbf"] / 60); + }, + + update: func { + me.fired = getprop(me._time_prop) > me.fire_time; + } +}; + +## +# Trigger object that will fire exactly after the given timeout. + +var TimeoutTrigger = { + + parents: [FailureMgr.Trigger], + # TODO: make this trigger async + requires_polling: 1, + + new: func(timeout) { + var m = FailureMgr.Trigger.new(); + m.parents = [TimeoutTrigger]; + m.params["timeout-sec"] = timeout; + fire_time = 0; + return m; + }, + + reset: func { + call(FailureMgr.Trigger.reset, [], me); + # TODO: use an elapsed time prop that accounts for speed-up and pause + me.fire_time = getprop("/sim/time/elapsed-sec") + + me.params["timeout-sec"]; + }, + + to_str: func { + sprintf("Fixed delay: %d minutes", me.params["timeout-sec"] / 60); + }, + + update: func { + me.fired = getprop("/sim/time/elapsed-sec") > me.fire_time; + } +}; + +## +# Simple approach to count usage cycles for a given property. Every time +# the propery variation changes in direction, we count half a cycle. +# If the property represents aileron angular position, for example, this +# would count roughly the number of times the aileron has been actuated. + +var CycleCounter = { + + new: func(property, on_update = nil) { + return { + parents: [CycleCounter], + cycles: 0, + _property: property, + _on_update: on_update, + _prev_value: getprop(property), + _prev_delta: 0, + _lsnr: nil + }; + }, + + enable: func { + if (me._lsnr == nil) + me._lsnr = setlistener(me._property, func (p) me._on_prop_change(p), 0, 0); + }, + + disable: func { + if (me._lsnr != nil) removelistener(me._lsnr); + }, + + reset: func { + me.cycles = 0; + me._prev_value = getprop(me._property); + me._prev_delta = 0; + }, + + _on_prop_change: func(prop) { + + # TODO: Implement a filter for avoiding spureous values. + + var value = prop.getValue(); + var delta = value - me._prev_value; + if (delta == 0) return; + + if (delta * me._prev_delta < 0) { + # Property variation has changed direction + me.cycles += 0.5; + if (me._on_update != nil) me._on_update(me.cycles); + } + + me._prev_delta = delta; + me._prev_value = value; + } +}; + +## +# Trigger object that will fire on average after a property has gone through +# mcbf (mean cycles between failures) cycles. + +var McbfTrigger = { + + parents: [FailureMgr.Trigger], + requires_polling: 0, + + new: func(property, mcbf) { + var m = FailureMgr.Trigger.new(); + m.parents = [McbfTrigger]; + m.params["mcbf"] = mcbf; + m.counter = CycleCounter.new(property, func(c) call(m._on_cycle, [c], m)); + m.activation_cycles = 0; + m.enabled = 0; + return m; + }, + + enable: func { + me.counter.enable(); + me.enabled = 1; + }, + + disable: func { + me.counter.disable(); + me.enabled = 0; + }, + + reset: func { + call(FailureMgr.Trigger.reset, [], me); + me.counter.reset(); + me.activation_cycles = + norm_rand(me.params["mcbf"], me.params["mcbf"] / 10); + + me.enabled and me.counter.enable(); + }, + + to_str: func { + sprintf("Mean cycles between failures: %.2f", me.params["mcbf"]); + }, + + _on_cycle: func(cycles) { + if (!me.fired and cycles > me.activation_cycles) { + # TODO: Why this doesn't work? + # me.counter.disable(); + me.fired = 1; + me.on_fire(); + } + } +}; diff --git a/Aircraft/Instruments-3d/GPSmap196/pages/page-panel.svg b/Aircraft/Instruments-3d/GPSmap196/pages/page-panel.svg index f440edc67..838bcff17 100644 --- a/Aircraft/Instruments-3d/GPSmap196/pages/page-panel.svg +++ b/Aircraft/Instruments-3d/GPSmap196/pages/page-panel.svg @@ -25,9 +25,6 @@ - + ft - 0 0 + 25 25 + 50 50 + 75 75 + 100 100 + 125 125 + 150 150 + 175 175 + @@ -693,129 +648,69 @@ transform="matrix(-0.95282935,0.30350655,-0.30805757,-0.95136772,0,0)" id="rect4476" style="fill:#000000;fill-opacity:1;stroke:none" /> - 0 0 + 1 1 + 2 2 + 3 3 + 4 4 + 5 5 + 6 6 + 7 7 + 8 8 + 9 + style="font-size:16px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans">9 + @@ -895,228 +790,121 @@ id="rect4643" style="fill:#000000;fill-opacity:1;stroke:none" /> - 000 kt 000 + kmh kmh + kt + waypoint waypoint + wpt-dist wpt-dist + 0000 0000 + ft m m + 00:00 00:00 + 0.00 0.00 + n n + s + k k + s m n m + k k + m n + ETE m + ETE + @@ -1453,160 +1241,89 @@ transform="matrix(-0.03999549,-0.99919986,0.99919986,-0.03999549,0,0)" id="rect3310" style="fill:#000000;fill-opacity:1;stroke:none" /> - 20 20 + 15 15 + 10 10 + 5 5 + 0 0 + 15 15 + 10 10 + 5 - 5 + + N N + S S + W W + E E + - 3 3 + 6 6 + 12 12 + 15 15 + 21 21 + 24 24 + 33 30 + 30 12 33 + + diff --git a/Nasal/FailureMgr/private.nas b/Nasal/FailureMgr/private.nas new file mode 100644 index 000000000..d124acf95 --- /dev/null +++ b/Nasal/FailureMgr/private.nas @@ -0,0 +1,327 @@ +# Failure Manager implementation +# +# Monitors trigger conditions periodically and fires failure modes when those +# conditions are met. It also provides a central access point for publishing +# failure modes to the user interface and the property tree. +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +## +# Represents one way things can go wrong, for example "a blown tire". + +var FailureMode = { + + ## + # id: Unique identifier for this failure mode. + # eg: "engine/carburetor-ice" + # + # description: Short text description, suitable for printing to the user. + # eg: "Ice in the carburetor" + # + # actuator: Object implementing the FailureActuator interface. + # Used by the failure manager to apply a certain level of + # failure to the failure mode. + + new: func(id, description, actuator) { + return { + parents: [FailureMode], + id: id, + description: description, + actuator: actuator, + _path: nil + }; + }, + + ## + # Applies a certain level of failure to this failure mode. + # level: Floating point number in the range [0, 1] zero being no failure + # and 1 total failure. + + set_failure_level: func(level) { + me._path != nil or + die("FailureMode.set_failure_level: Unbound failure mode"); + + setprop(me._path ~ me.id ~ "/failure-level", level); + }, + + ## + # Internal version that actually does the job. + + _set_failure_level: func(level) { + me.actuator.set_failure_level(level); + me._log_failure(sprintf("%s failure level %d%%", + me.description, level*100)); + }, + + ## + # Returns the level of failure currently being simulated. + + get_failure_level: func me.actuator.get_failure_level(), + + ## + # Creates an interface for this failure mode in the property tree at the + # given location. Currently the interface is just: + # + # path/failure-level (double, rw) + + bind: func(path) { + me._path == nil or die("FailureMode.bind: mode already bound"); + + var prop = path ~ me.id ~ "/failure-level"; + props.globals.initNode(prop, me.actuator.get_failure_level(), "DOUBLE"); + setlistener(prop, func (p) me._set_failure_level(p.getValue()), 0, 0); + me._path = path; + }, + + ## + # Remove bound properties from the property tree. + + unbind: func { + me._path != nil and props.globals.getNode(me._path ~ me.id).remove(); + me._path = nil; + }, + + ## + # Send a message to the logging facilities, currently the screen and + # the console. + + _log_failure: func(message) { + print(getprop("/sim/time/gmt-string") ~ " : " ~ message); + if (getprop(proproot ~ "/display-on-screen")) + screen.log.write(message, 1.0, 0.0, 0.0); + }, +}; + +## +# Implements the FailureMgr functionality. +# +# It is wrapped into an object to leave the door open to several evolution +# approaches, for example moving the implementation down to the C++ engine, +# or supporting several independent instances of the failure manager. +# Additionally, it also serves to isolate low level implementation details +# into its own namespace. + +var _failmgr = { + + timer: nil, + update_period: 10, # 0.1 Hz + failure_modes: {}, + pollable_trigger_count: 0, + + init: func { + me.timer = maketimer(me.update_period, func me._update()); + setlistener("sim/signals/reinit", func me._on_reinit()); + + props.globals.initNode(proproot ~ "display-on-screen", 1, "BOOL"); + props.globals.initNode(proproot ~ "enabled", 1, "BOOL"); + setlistener(proproot ~ "enabled", + func (n) { n.getValue() ? me._enable() : me._disable() }); + }, + + ## + # Subscribe a new failure mode to the system. + # mode: FailureMode object. + + add_failure_mode: func(mode) { + contains(me.failure_modes, mode.id) and + die("add_failure_mode: failure mode already exists: " ~ id); + + me.failure_modes[mode.id] = { mode: mode, trigger: nil }; + mode.bind(proproot); + }, + + ## + # Remove a failure mode from the system. + # id: FailureMode id string, e.g. "systems/pitot" + + remove_failure_mode: func(id) { + contains(me.failure_modes, id) or + die("remove_failure_mode: failure mode does not exist: " ~ mode_id); + + var trigger = me.failure_modes[id].trigger; + if (trigger != nil) + me._discard_trigger(trigger); + + me.failure_modes[id].unbind(); + props.globals.getNode(proproot ~ id).remove(); + delete(me.failure_modes, id); + }, + + ## + # Removes all failure modes from the system. + + remove_all: func { + foreach(var id; keys(me.failure_modes)) + me.remove_failure_mode(id); + }, + + ## + # Attach a trigger to the given failure mode. Discards the current trigger + # if any. + # + # mode_id: FailureMode id string, e.g. "systems/pitot" + # trigger: Trigger object or nil. + + set_trigger: func(mode_id, trigger) { + contains(me.failure_modes, mode_id) or + die("set_trigger: failure mode does not exist: " ~ mode_id); + + var mode = me.failure_modes[mode_id]; + + if (mode.trigger != nil) + me._discard_trigger(mode.trigger); + + mode.trigger = trigger; + if (trigger == nil) return; + + trigger.bind(proproot ~ mode_id); + trigger.on_fire = func _failmgr.on_trigger_activated(trigger); + trigger.reset(); + + if (trigger.requires_polling) { + me.pollable_trigger_count += 1; + + if (me.enabled() and !me.timer.isRunning) + me.timer.start(); + } + + trigger.enable(); + }, + + ## + # Returns the trigger object attached to the given failure mode. + # mode_id: FailureMode id string, e.g. "systems/pitot" + + get_trigger: func(mode_id) { + contains(me.failure_modes, mode_id) or + die("get_trigger: failure mode does not exist: " ~ mode_id); + + return me.failure_modes[mode_id].trigger; + }, + + ## + # Observer interface. Called from asynchronous triggers when they fire. + # trigger: Reference to the calling trigger. + + on_trigger_activated: func(trigger) { + var found = 0; + + foreach (var id; keys(me.failure_modes)) { + if (me.failure_modes[id].trigger == trigger) { + me.failure_modes[id].mode.set_failure_level(1); + found = 1; + break; + } + } + + found or die("FailureMgr.on_trigger_activated: trigger not found"); + }, + + ## + # Enable the failure manager. + + _enable: func { + foreach(var id; keys(me.failure_modes)) { + var trigger = me.failure_modes[id].trigger; + trigger != nil and trigger.enable(); + } + + if (me.pollable_trigger_count > 0) + me.timer.start(); + }, + + ## + # Suspends failure manager activity. Pollable triggers will not be updated + # and all triggers will be disabled. + + _disable: func { + me.timer.stop(); + + foreach(var id; keys(me.failure_modes)) { + var trigger = me.failure_modes[id].trigger; + trigger != nil and trigger.disable(); + } + + }, + + ## + # Returns enabled status. + + enabled: func { + getprop(proproot ~ "enabled"); + }, + + ## + # Poll loop. Updates pollable triggers and applies a failure level + # when they fire. + + _update: func { + foreach (var id; keys(me.failure_modes)) { + var failure = me.failure_modes[id]; + + if (failure.trigger != nil and !failure.trigger.fired) { + var level = failure.trigger.update(); + if (level > 0 and level != failure.mode.get_failure_level()) + failure.mode.set_failure_level(level); + } + } + }, + + ## + # Detaches a trigger from the system. + + _discard_trigger: func(trigger) { + trigger.disable(); + trigger.unbind(); + + if (trigger.requires_polling) { + me.pollable_trigger_count -= 1; + me.pollable_trigger_count == 0 and me.timer.stop(); + } + }, + + ## + # Reinit listener. Sets all failure modes to "working fine". + + _on_reinit: func { + foreach (var id; keys(me.failure_modes)) { + var failure = me.failure_modes[id]; + + failure.mode.set_failure_level(0); + + if (failure.trigger != nil) { + me._discard_trigger(failure.trigger); + failure.trigger = nil; + } + } + } +}; + +## +# Module initialization + +var _init = func { + removelistener(lsnr); + _failmgr.init(); + + # Load legacy failure modes for backwards compatibility + io.load_nasal(getprop("/sim/fg-root") ~ + "/Aircraft/Generic/Systems/compat_failure_modes.nas"); +} + +var lsnr = setlistener("/nasal/FailureMgr/loaded", _init); diff --git a/Nasal/FailureMgr/public.nas b/Nasal/FailureMgr/public.nas new file mode 100644 index 000000000..957bb54a5 --- /dev/null +++ b/Nasal/FailureMgr/public.nas @@ -0,0 +1,229 @@ +# Failure Manager public interface +# +# Copyright (C) 2014 Anton Gomez Alvedro +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +var proproot = "sim/failure-manager/"; + + +## +# Subscribe a new failure mode to the system. +# +# id: Unique identifier for this failure mode. +# eg: "engine/carburetor-ice" +# +# description: Short text description, suitable for printing to the user. +# eg: "Ice in the carburetor" +# +# actuator: Object implementing the FailureActuator interface. +# Used by the failure manager to apply a certain level of +# failure to the failure mode. + +var add_failure_mode = func(id, description, actuator) { + _failmgr.add_failure_mode( + FailureMode.new(id, description, actuator)); +} + +## +# Remove a failure mode from the system. +# id: FailureMode id string, e.g. "systems/pitot" + +var remove_failure_mode = func(id) { + _failmgr.remove_failure_mode(id); +} + +## +# Removes all failure modes from the failure manager. + +var remove_all = func { + _failmgr.remove_all(); +} + +## +# Attaches a trigger to the given failure mode. Discards the current trigger +# if any. +# +# mode_id: FailureMode id string, e.g. "systems/pitot" +# trigger: Trigger object or nil. Nil will just detach the current trigger + +var set_trigger = func(mode_id, trigger) { + _failmgr.set_trigger(mode_id, trigger); +} + +## +# Returns the trigger object attached to the given failure mode. +# mode_id: FailureMode id string, e.g. "systems/pitot" + +var get_trigger = func(mode_id) { + _failmgr.get_trigger(mode_id); +} + +## +# Applies a certain level of failure to this failure mode. +# +# mode_id: Failure mode id string. +# level: Floating point number in the range [0, 1] +# Zero represents no failure and one means total failure. + +var set_failure_level = func (mode_id, level) { + setprop(proproot ~ mode_id ~ "/failure-level", level); +} + +## +# Allows applications to disable the failure manager and restore it later on. +# While disabled, no failure modes will be activated from the failure manager. + +var enable = func setprop(proproot ~ "enabled", 1); +var disable = func setprop(proproot ~ "enabled", 0); + +## +# Encapsulates a condition that when met, will make the failure manager to +# apply a certain level of failure to the failure mode it is bound to. +# +# Two types of triggers are supported: pollable and asynchronous. +# +# Pollable triggers require periodic check for trigger conditions. For example, +# an altitude trigger will need to poll current altitude until the fire +# condition is reached. +# +# Asynchronous trigger do not require periodic updates. They can detect +# the firing condition by themselves by using timers or listeners. +# Async triggers must call the inherited method on_fire() to let the Failure +# Manager know about the fired condition. +# +# See Aircraft/Generic/Systems/failures.nas for concrete examples of triggers. + +var Trigger = { + + # 1 for pollable triggers, 0 for async triggers. + requires_polling: 0, + + new: func { + return { + parents: [Trigger], + params: {}, + fired: 0, + + ## + # Async triggers shall call the on_fire() callback when their fire + # conditions are met to notify the failure manager. + on_fire: func 0, + + _path: nil + }; + }, + + ## + # Enables/disables the trigger. While a trigger is disabled, any timer + # or listener that could potentially own shall be disabled. + + enable: func, + disable: func, + + ## + # Forces a check of the firing conditions. Returns 1 if the trigger fired, + # 0 otherwise. + + update: func 0, + + ## + # Returns a printable string describing the trigger condition. + + to_str: func "undefined trigger", + + ## + # Modify a trigger parameter. Parameters will take effect after the next + # call to reset() + + set_param: func(param, value) { + contains(me.params, param) or + die("Trigger.set_param: undefined param: " ~ param); + + me._path != nil or + die("Trigger.set_param: Unbound trigger"); + + setprop(sprintf("%s/%s",me._path, param), value); + }, + + ## + # Reload trigger parameters and reset internal state, i.e. start from + # scratch. If the trigger was fired, the trigger is set to not fired. + + reset: func { + me._path or die("Trigger.reset: unbound trigger"); + + foreach (var p; keys(me.params)) + me.params[p] = getprop(sprintf("%s/%s", me._path, p)); + + me.fired = 0; + me._path != nil and setprop(me._path ~ "/reset", 0); + }, + + ## + # Creates an interface for the trigger in the property tree. + # Every parameter in the params hash will be exposed, in addition to + # a path/reset property for resetting the trigger from the prop tree. + + bind: func(path) { + me._path == nil or + die("Trigger.bind(): attempt to bind an already bound trigger"); + + me._path = path; + props.globals.getNode(path) != nil or props.globals.initNode(path); + props.globals.getNode(path).setValues(me.params); + + var reset_prop = path ~ "/reset"; + props.globals.initNode(reset_prop, 0, "BOOL"); + setlistener(reset_prop, func me.reset(), 0, 0); + }, + + ## + # Removes this trigger's interface from the property tree. + + unbind: func { + props.globals.getNode(me._path ~ "/reset").remove(); + foreach (var p; keys(me.params)) + props.globals.getNode(me._path ~ "/" ~ p).remove(); + + me._path = nil; + } +}; + +## +# FailureActuators encapsulate the actions required for activating the actual +# failure simulation. +# +# Traditionally this action was just manipulating a "serviceable" property +# somewhere, but the FailureActuator gives you more flexibility, allowing you +# to touch several properties at once or call other Nasal scripts, for example. +# +# See Aircraft/Generic/Systems/failure.nas and +# Aircraft/Generic/Systems/compat_failures.nas for some examples of actuators. + +var FailureActuator = { + + ## + # Called from the failure manager to activate a certain level of failure. + # level: Target level of failure [0 to 1]. + + set_failure_level: func(level) 0, + + ## + # Returns the level of failure that is currently being simulated. + + get_failure_level: func 0, +}; diff --git a/Nasal/atc-chatter/atc-chatter.nas b/Nasal/atc-chatter/atc-chatter.nas deleted file mode 100644 index 7819633b3..000000000 --- a/Nasal/atc-chatter/atc-chatter.nas +++ /dev/null @@ -1,115 +0,0 @@ -############################################################################# -# -# Simple sequenced ATC background chatter function -# -# Written by Curtis Olson -# Started 8 Jan 2006. -# -############################################################################# - -############################################################################# -# Global shared variables -############################################################################# - -var fg_root = nil; -var chatter = "UK"; -var chatter_dir = ""; - -var chatter_min_interval = 20.0; -var chatter_max_interval = 40.0; -var next_interval = nil; - -var chatter_index = 0; -var chatter_size = 0; -var chatter_list = 0; - - -############################################################################# -# Chatter is initialized only when actually enabled. See listener connected -# to /sim/sound/chatter/enabled. -############################################################################# - -var chatter_init = func { - # default values - fg_root = getprop("/sim/fg-root"); - chatter_dir = sprintf("%s/ATC/Chatter/%s", fg_root, chatter); - chatter_list = directory( chatter_dir ); - chatter_size = size(chatter_list); - # seed the random number generator (with time) so we don't start in - # same place in the sequence each run. - srand(); - chatter_index = int( chatter_size * rand() ); -} - - -############################################################################# -# main update function to be called each frame -############################################################################# - -var chatter_update = func { - if ( chatter_index >= chatter_size ) { - chatter_index = 0; - } - - if ( substr(chatter_list[chatter_index], - size(chatter_list[chatter_index]) - 4) == ".wav" ) - { - var vol =getprop("/sim/sound/chatter/volume"); - if(vol == nil){vol = 0.5;} - tmpl = { path : chatter_dir, file : chatter_list[chatter_index] , volume : vol}; - if ( getprop("/sim/sound/chatter/enabled") ) { - # go through the motions, but only schedule the message to play - # if atc-chatter is enabled. - printlog("info", "update atc chatter ", chatter_list[chatter_index] ); - fgcommand("play-audio-sample", props.Node.new(tmpl) ); - } - } else { - # skip non-wav file found in directory - } - - chatter_index = chatter_index + 1; - nextChatter(); -} - - -############################################################################# -# Use the nasal timer to update every 10 seconds -############################################################################# - -var nextChatter = func { - if (!getprop("/sim/sound/chatter/enabled")) - { - next_interval = nil; - return; - } - - # schedule next message in next min-max interval seconds so we have a bit - # of a random pacing - next_interval = chatter_min_interval - + int(rand() * (chatter_max_interval - chatter_min_interval)); - - # printlog("info", "next chatter in ", next_interval, " seconds"); - - settimer(chatter_update, next_interval ); -} - -############################################################################# -# Start chatter processing. Also connected to chatter/enabled property as a -# listener. -############################################################################# - -var startChatter = func { - if ( getprop("/sim/sound/chatter/enabled") ) { - if (fg_root == nil) - chatter_init(); - if (next_interval == nil) - nextChatter(); - } -} - -# connect listener -_setlistener("/sim/sound/chatter/enabled", startChatter); - -# start chatter immediately, if enable is already set. -settimer(startChatter, 0); - diff --git a/Nasal/canvas/MapStructure.nas b/Nasal/canvas/MapStructure.nas index 1bc20fe01..bc87c2d2c 100644 --- a/Nasal/canvas/MapStructure.nas +++ b/Nasal/canvas/MapStructure.nas @@ -2,29 +2,94 @@ ## MapStructure mapping/charting framework for Nasal/Canvas, by Philosopher ## See: http://wiki.flightgear.org/MapStructure ############################################################################### + + +####### +## Dev Notes: +## +## - consider adding two types of SymbolLayers (sub-classes): Static (fixed positions, navaids/fixes) Dynamic (frequently updated, TFC/WXR, regardless of aircraft position) +## - FLT should be managed by aircraftpos.controller probably (interestign corner case actually) +## - consider adding an Overlay, i.e. for things like compass rose, lat/lon coordinate grid, but also tiled map data fetched on line +## - consider patching svg.nas to allow elements to be styled via the options hash by rewriting attributes, could even support animations that way +## - style handling/defaults should be moved to symbol files probably +## - consider pre-populating layer environments via bind() by providing APIs and fields for sane defaults: +## - parents +## - __self__ +## - del (managing all listeners and timers) +## - searchCmd -> filtering +## +## APIs to be wrapped for each layer: +## printlog(), die(), debug.bt(), benchmark() + var _MP_dbg_lvl = "info"; #var _MP_dbg_lvl = "alert"; -var dump_obj = func(m) { - var h = {}; - foreach (var k; keys(m)) - if (k != "parents") - h[k] = m[k]; - debug.dump(h); -}; +var makedie = func(prefix) func(msg) globals.die(prefix~" "~msg); + +var __die = makedie("MapStructure"); ## -# for LOD handling (i.e. airports/taxiways/runways) -var RangeAware = { - new: func { - return {parents:[RangeAware] }; - }, - del: func, - notifyRangeChange: func die("RangeAware.notifyRangeChange() must be provided by sub-class"), -}; +# Try to call a method on an object with no arguments. Should +# work for both ghosts and hashes; catches the error only when +# the method doesn't exist -- errors raised during the call +# are re-thrown. +# +var try_aux_method = func(obj, method_name) { + var name = ""; + call(compile("obj."~method_name~"()", name), nil, var err=[]); # try... + #debug.dump(err); + if (size(err)) # ... and either leave caght or rethrow + if (err[1] != name) + die(err[0]); +} -## wrapper for each cached element -## i.e. keeps the canvas and texture map coordinates for the corresponding raster image +## +# Combine a specific hash with a default hash, e.g. for +# options/df_options and style/df_style in a SymbolLayer. +# +var default_hash = func(opt, df) { + if (opt != nil and typeof(opt)=='hash') { + if (df != nil and opt != df and !isa(opt, df)) { + if (contains(opt, "parents")) + opt.parents ~= [df]; + else + opt.parents = [df]; + } + return opt; + } else return df; +} + +## +# to be used for prototyping, performance & stress testing (especially with multiple instance driven by AI traffic) +# + +var MapStructure_selfTest = func() { + var temp = {}; + temp.dlg = canvas.Window.new([600,400],"dialog"); + temp.canvas = temp.dlg.createCanvas().setColorBackground(1,1,1,0.5); + temp.root = temp.canvas.createGroup(); + var TestMap = temp.root.createChild("map"); + TestMap.setController("Aircraft position"); + TestMap.setRange(25); # TODO: implement zooming/panning via mouse/wheel here, for lack of buttons :-/ + TestMap.setTranslation( + temp.canvas.get("view[0]")/2, + temp.canvas.get("view[1]")/2 + ); + var r = func(name,vis=1,zindex=nil) return caller(0)[0]; + # TODO: we'll need some z-indexing here, right now it's just random + # TODO: use foreach/keys to show all layers in this case by traversing SymbolLayer.registry direclty ? + # maybe encode implicit z-indexing for each lcontroller ctor call ? - i.e. preferred above/below order ? + foreach(var type; [r('TFC',0),r('APT'),r('DME'),r('VOR'),r('NDB'),r('FIX',0),r('RTE'),r('WPT'),r('FLT'),r('WXR'),r('APS'), ] ) + TestMap.addLayer(factory: canvas.SymbolLayer, type_arg: type.name, + visible: type.vis, priority: type.zindex, + ); +}; # MapStructure_selfTest + + +## +# wrapper for each cached element: keeps the canvas and +# texture map coordinates for the corresponding raster image. +# var CachedElement = { new: func(canvas_path, name, source, size, offset) { var m = {parents:[CachedElement] }; @@ -37,19 +102,18 @@ var CachedElement = { m.size = size; m.offset = offset; return m; - }, # new() - + }, + render: func(group, trans0=0, trans1=0) { # create a raster image child in the render target/group - var n = group.createChild("image", me.name) - .setFile( me.canvas_src ) - # TODO: fix .setSourceRect() to accept a single vector for texture map coordinates ... - .setSourceRect(left:me.source[0],top:me.source[1],right:me.source[2],bottom:me.source[3], normalized:0) + var n = group.createChild("image", me.name) + .setFile(me.canvas_src) + .setSourceRect(me.source, 0) .setSize(me.size) .setTranslation(trans0,trans1); n.createTransform().setTranslation(me.offset); return n; - }, # render() + }, }; # of CachedElement var SymbolCache = { @@ -98,25 +162,34 @@ var SymbolCache = { m.path = m.canvas_texture.getPath(); m.root = m.canvas_texture.createGroup("entries"); + # TODO: register a reset/re-init listener to optionally purge/rebuild the cache ? + return m; }, + ## + # Add a cached symbol based on a drawing callback. + # @note this assumes that the object added by callback + # fits into the dimensions provided to the constructor, + # and any larger dimensionalities are liable to be cut off. + # add: func(name, callback, draw_mode=0) { if (typeof(draw_mode) == 'scalar') var draw_mode0 = var draw_mode1 = draw_mode; else var (draw_mode0,draw_mode1) = draw_mode; + # get canvas texture that we use as cache # get next free spot in texture (column/row) - # run the draw callback and render into a group + # run the draw callback and render into a new group var gr = me.root.createChild("group",name); - gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0, - me.next_free[1] + me.image_sz[1]*draw_mode1); - #settimer(func debug.dump ( gr.getTransformedBounds() ), 0); # XXX: these are only updated when rendered - #debug.dump ( gr.getTransformedBounds() ); - gr.update(); # apparently this doesn't result in sane output from .getTransformedBounds() either - #debug.dump ( gr.getTransformedBounds() ); - # draw the symbol + # draw the symbol into the group callback(gr); - # get the bounding box, i.e. coordinates for texture map, or use the .setTranslation() params + + gr.update(); # if we need sane output from getTransformedBounds() + #debug.dump ( gr.getTransformedBounds() ); + gr.setTranslation( me.next_free[0] + me.image_sz[0]*draw_mode0, + me.next_free[1] + me.image_sz[1]*draw_mode1); + + # get assumed the bounding box, i.e. coordinates for texture map var coords = me.next_free~me.next_free; foreach (var i; [0,1]) coords[i+1] += me.image_sz[i]; @@ -130,7 +203,9 @@ var SymbolCache = { if (me.next_free[0] >= me.canvas_sz[0]) { me.next_free[0] = 0; me.next_free[1] += me.image_sz[1] } if (me.next_free[1] >= me.canvas_sz[1]) - die("SymbolCache: ran out of space after adding '"~name~"'"); + __die("SymbolCache: ran out of space after adding '"~name~"'"); + + if (contains(me.dict, name)) print("MapStructure/SymbolCache Warning: Overwriting existing cache entry named:", name); # store texture map coordinates in lookup map using the name as identifier return me.dict[name] = CachedElement.new( @@ -142,11 +217,172 @@ var SymbolCache = { ); }, # add() get: func(name) { - if(!contains(me.dict,name)) die("No SymbolCache entry for key:"~ name); return me.dict[name]; }, # get() }; +# Excerpt from gen module +var denied_symbols = [ + "", "func", "if", "else", "var", + "elsif", "foreach", "for", + "forindex", "while", "nil", + "return", "break", "continue", +]; +var issym = func(string) { + foreach (var d; denied_symbols) + if (string == d) return 0; + var sz = size(string); + var s = string[0]; + if ((s < `a` or s > `z`) and + (s < `A` or s > `Z`) and + (s != `_`)) return 0; + for (var i=1; i `z`) and + (s < `A` or s > `Z`) and + (s < `0` or s > `9`)) return 0; + return 1; +}; +var internsymbol = func(symbol) { + #assert("argument not a symbol", issym, symbol); + if (!issym(symbol)) die("argument not a symbol"); + var get_interned = compile(""" + keys({"~symbol~":})[0] + """); + return get_interned(); +}; +var tryintern = func(symbol) issym(symbol) ? internsymbol(symbol) : symbol; + +# End excerpt + +# Helpers for below +var unescape = func(s) string.replace(s~"", "'", "\\'"); +var hashdup = func(_,rkeys=nil) { + var h={}; var k=rkeys!=nil?rkeys:members(_); + foreach (var k;k) h[tryintern(k)]=member(_,k); h +} +var member = func(h,k) { + if (contains(h, k)) return h[k]; + if (contains(h, "parents")) { + var _=h.parents; + for (var i=0;i(