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..4bcc415e3 --- /dev/null +++ b/Aircraft/Generic/Systems/failures.nas @@ -0,0 +1,363 @@ +# 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"; + + 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 + var std = math.sqrt(me.params["mtbf"] / 10 - 1); + me.fire_time = getprop(me._time_prop) + + norm_rand(me.params["mtbf"], std); + }, + + 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"], math.sqrt(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/Nasal/FailureMgr/private.nas b/Nasal/FailureMgr/private.nas new file mode 100644 index 000000000..ca0257dd9 --- /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 me.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/failures.nas b/Nasal/failures.nas deleted file mode 100644 index 7c13673e8..000000000 --- a/Nasal/failures.nas +++ /dev/null @@ -1,230 +0,0 @@ -# failures.nas a manager for failing systems based on MTBF/MCBF - -# Time between MTBF checks -var dt = 10; - -# Root property for failure information -var failure_root = "/sim/failure-manager"; - -# Enumerations -var type = { MTBF : 1, MCBF: 2 }; -var fail = { SERVICEABLE : 1, JAM : 2, ENGINE: 3}; - -# This hash contains a mapping from property entry to a failure object -# containing the following members: -# type: MTBF|MCBF Mean Time Between Failures/Mean Cycle Between Failures -# desc: Description of property for screen output -# failure: SERVICEABLE Property has a "serviceable" child that can be set to false -# failure: JAM Property is failed by marking as Read-Only -# failure: ENGINE Special case for engines, where a variety of properties are set. -# failure: Property is failed by setting another property to false -var breakHash = { - "/instrumentation/adf" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "ADF" }, - "/instrumentation/dme" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "DME" }, - "/instrumentation/airspeed-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "ASI" }, - "/instrumentation/altimeter" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Altimeter" }, - "/instrumentation/attitude-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Attitude Indicator" }, - "/instrumentation/heading-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Heading Indicator" }, - "/instrumentation/magnetic-compass" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Magnetic Compass" }, - "/instrumentation/nav[0]/gs" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 1 Glideslope" }, - "/instrumentation/nav[0]/cdi" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 1 CDI" }, - "/instrumentation/nav[1]/gs" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 2 Glideslope" }, - "/instrumentation/nav[1]/cdi" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Nav 2 CDI" }, - "/instrumentation/slip-skid-ball" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Slip/Skid Ball" }, - "/instrumentation/turn-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Turn Indicator" }, - "/instrumentation/vertical-speed-indicator" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "VSI" }, - "/systems/electrical" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Electrical system" }, - "/systems/pitot" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Pitot system" }, - "/systems/static" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Static system" }, - "/systems/vacuum" : { type: type.MTBF, failure: fail.SERVICEABLE, desc: "Vacuum system" }, - "/controls/gear/gear-down" : { type: type.MCBF, failure: "/gear/serviceable", desc: "Gear" }, - "/controls/flight/aileron" : { type: type.MTBF, failure: fail.JAM, desc: "Aileron" }, - "/controls/flight/elevator" : { type: type.MTBF, failure: fail.JAM, desc: "Elevator" }, - "/controls/flight/rudder" : { type: type.MTBF, failure: fail.JAM, desc: "Rudder" }, - "/controls/flight/flaps" : { type: type.MCBF, failure: fail.JAM, desc: "Flaps" }, - "/controls/flight/speedbrake" : { type: type.MCBF, failure: fail.JAM, desc: "Speed Brake" } -}; - -# Return the failure entry for a given property -var getFailure = func (prop) { - var o = breakHash[prop]; - - if (o.failure == fail.SERVICEABLE) { - return prop ~ "/serviceable"; - } elsif (o.failure == fail.ENGINE) { - return failure_root ~ prop ~ "/serviceable"; - } elsif (o.failure == fail.JAM) { - return failure_root ~ prop ~ "/serviceable"; - } else { - return o.failure; - } -} - -# Fail a given property, either using a serviceable flag, or by jamming the property -var failProp = func(prop) { - - var o = breakHash[prop]; - var p = getFailure(prop); - - if (getprop(p) == 1) { - setprop(p, 0); - - # We always print to the console - print(getprop("/sim/time/gmt-string") ~ " : " ~ o.desc ~ " failed"); - - if (getprop(failure_root ~ "/display-on-screen")) { - # Display message to the screen in red - screen.log.write(o.desc ~ " failed", 1.0, 0.0, 0.0); - } - } -} - -# Unfail a given property, used for resetting a failure state. -var unfailProp = func(prop) -{ - var p = getFailure(prop); - setprop(p, 1); -} - -# Unfail all the failed properties -var unfail = func { - foreach(var prop; keys(breakHash)) { - unfailProp(prop); - } -} - -# Listener to jam a property. Note that the property to jam is -# encoded within the property name -var jamListener = func(p) { - var jamprop = string.replace(p.getParent().getPath(), failure_root, ""); -#jamprop = string.replace(jamprop, "/serviceable", ""); - var prop = props.globals.getNode(jamprop); - if (p.getValue()) { - prop.setAttribute("writable", 1); - } else { - prop.setAttribute("writable", 0); - } -} - -# Listener for an engine property. Note that the engine to set is -# encoded within the property name. We set both the magnetos and -# cutoff to handle different engine models. -var engineListener = func(p) { - var e = string.replace(p.getParent().getPath(), failure_root, ""); - var prop = props.globals.getNode(e); - if (p.getValue()) { - # Enable the properties, but don't set the magnetos, as they may - # be off for a reason. - var magnetos = props.globals.getNode("/controls/" ~ e ~ "/magnetos", 1); - var cutoff = props.globals.getNode("/controls/" ~ e ~ "/cutoff", 1); - magnetos.setAttribute("writable", 1); - cutoff.setAttribute("writable", 1); - cutoff.setValue(0); - } else { - # Switch off the engine, and disable writing to it. - var magnetos = props.globals.getNode("/controls/" ~ e ~ "/magnetos", 1); - var cutoff = props.globals.getNode("/controls/" ~ e ~ "/cutoff", 1); - magnetos.setValue(0); - cutoff.setValue(1); - magnetos.setAttribute("writable", 0); - cutoff.setAttribute("writable", 0); - } -} - -# Perform a MCBF check against a failure property. -var checkMCBF = func(prop) { - var mcbf = getprop(failure_root ~ prop.getPath() ~ "/mcbf"); - # mcbf == mean cycles between failures - # hence 2*mcbf is the number of _half-cycles_ between failures, - # which is relevant because we do this check on each half-cycle: - if ((mcbf > 0) and !int(2 * mcbf * rand())) { - # Get the property information. - failProp(prop.getPath()); - } -} - -# Timer based loop to check MTBF properties -var checkMTBF = func { - foreach(var prop; keys(breakHash)) { - var o = breakHash[prop]; - if (o.type == type.MTBF) { - var mtbf = getprop(failure_root ~ prop ~ "/mtbf"); - if (mtbf and !int(rand() * mtbf / dt)) { - failProp(prop); - } - } - } - settimer(checkMTBF, dt); -} - -# Function to set all MTBF failures to a give value. Mainly for testing. -var setAllMTBF = func(mtbf) { - foreach(var prop; keys(breakHash)) { - var o = breakHash[prop]; - if (o.type == type.MTBF) { - setprop(failure_root ~ prop ~ "/mtbf", mtbf); - } - } -} - -# Function to set all MCBF failures to a give value. Mainly for testing. -var setAllMCBF = func(mcbf) { - foreach(var prop; keys(breakHash)) { - var o = breakHash[prop]; - if (o.type == type.MCBF) { - setprop(failure_root ~ prop ~ "/mcbf", mcbf); - } - } -} - -# Initialization, called once Nasal and the FDM are loaded properly. -var fdm_init_listener = _setlistener("/sim/signals/fdm-initialized", func { - removelistener(fdm_init_listener); # uninstall, so we're only called once - srand(); - - # Engines are added dynamically because there may be an arbitrary number - var i = 1; - foreach (var e; props.globals.getNode("/engines").getChildren("engine")) { - breakHash[e.getPath()] = { type: type.MTBF, failure: fail.ENGINE, desc : "Engine " ~ i }; - i = i+1; - } - - # Set up serviceable, MCBF and MTBF properties. - foreach(var prop; keys(breakHash)) { - var o = breakHash[prop]; - var t = "/mcbf"; - - if (o.type == type.MTBF) { - t = "/mtbf"; - } - - # Set up the MTBF/MCFB properties to 0. Note that they are in a separate - # subtree, as there's no guarantee that the property isn't a leaf. - props.globals.initNode(failure_root ~ prop ~ t, 0); - - if (o.failure == fail.SERVICEABLE) { - # If the property has a serviceable property, set it if appropriate. - props.globals.initNode(prop ~ "/serviceable", 1, "BOOL"); - } elsif (o.failure == fail.JAM) { - # In the JAM case, we actually have a dummy serviceable property for the GUI. - props.globals.initNode(failure_root ~ prop ~ "/serviceable", 1, "BOOL"); - setlistener(failure_root ~ prop ~ "/serviceable", jamListener); - } elsif (o.failure == fail.ENGINE) { - # In the JAM case, we actually have a dummy serviceable property for the GUI. - props.globals.initNode(failure_root ~ prop ~ "/serviceable", 1, "BOOL"); - setlistener(failure_root ~ prop ~ "/serviceable", engineListener); - } else { - # If the serviceable property is actually defined, check it is set. - props.globals.initNode(o.failure, 1, "BOOL"); - } - - if (o.type == type.MCBF) { - # Set up listener for MCBF properties, only when the value changes. - setlistener(prop, checkMCBF, 0, 0); - } - } - - # Start checking for failures. - checkMTBF(); -}); - diff --git a/gui/dialogs/instrument-failures.xml b/gui/dialogs/instrument-failures.xml index 1dc4cb1dd..8e67cc9d9 100644 --- a/gui/dialogs/instrument-failures.xml +++ b/gui/dialogs/instrument-failures.xml @@ -80,7 +80,7 @@ 1 1 - /instrumentation/nav[0]/cdi/serviceable + /sim/failure-manager/instrumentation/nav[0]/cdi/serviceable @@ -101,7 +101,7 @@ 1 5 - /instrumentation/nav[1]/cdi/serviceable + /sim/failure-manager/instrumentation/nav[1]/cdi/serviceable @@ -122,7 +122,7 @@ 2 1 - /instrumentation/nav[0]/gs/serviceable + /sim/failure-manager/instrumentation/nav[0]/gs/serviceable @@ -141,7 +141,7 @@ 2 5 - /instrumentation/nav[1]/gs/serviceable + /sim/failure-manager/instrumentation/nav[1]/gs/serviceable @@ -160,7 +160,7 @@ 3 1 - /instrumentation/dme/serviceable + /sim/failure-manager/instrumentation/dme/serviceable @@ -179,7 +179,7 @@ 3 5 - /instrumentation/adf/serviceable + /sim/failure-manager/instrumentation/adf/serviceable @@ -198,7 +198,7 @@ 4 1 - /instrumentation/airspeed-indicator/serviceable + /sim/failure-manager/instrumentation/airspeed-indicator/serviceable @@ -217,7 +217,7 @@ 4 5 - /instrumentation/attitude-indicator/serviceable + /sim/failure-manager/instrumentation/attitude-indicator/serviceable @@ -236,7 +236,7 @@ 5 1 - /instrumentation/altimeter/serviceable + /sim/failure-manager/instrumentation/altimeter/serviceable @@ -255,7 +255,7 @@ 5 5 - /instrumentation/turn-indicator/serviceable + /sim/failure-manager/instrumentation/turn-indicator/serviceable @@ -274,7 +274,7 @@ 6 1 - /instrumentation/slip-skid-ball/serviceable + /sim/failure-manager/instrumentation/slip-skid-ball/serviceable @@ -293,7 +293,7 @@ 6 5 - /instrumentation/heading-indicator/serviceable + /sim/failure-manager/instrumentation/heading-indicator/serviceable @@ -312,7 +312,7 @@ 7 1 - /instrumentation/vertical-speed-indicator/serviceable + /sim/failure-manager/instrumentation/vertical-speed-indicator/serviceable @@ -331,7 +331,7 @@ 7 5 - /instrumentation/magnetic-compass/serviceable + /sim/failure-manager/instrumentation/magnetic-compass/serviceable @@ -380,7 +380,7 @@ diff --git a/gui/dialogs/system-failures.xml b/gui/dialogs/system-failures.xml index 64be83cc4..3385bd5de 100644 --- a/gui/dialogs/system-failures.xml +++ b/gui/dialogs/system-failures.xml @@ -30,40 +30,44 @@ i += 1; foreach (var e; engines.getChildren("engine")) { - if (((e.getChild("starter") != nil) and (e.getChild("starter") != "")) or - ((e.getChild("running") != nil) and (e.getChild("running") != "")) ) { - row = row + 1; + var starter = e.getChild("starter"); + var running = e.getChild("running"); - # Set up the label - target = group.getNode("text[" ~ i ~ "]", 1); - props.copy(group.getNode("text-template"), target); - target.getNode("row").setValue(row); + (starter != nil and starter != "" and starter.getType() != "NONE") + or (running != nil and running != "" and running.getType() != "NONE") + or continue; - if (size(engines.getChildren("engine")) == 1) { - target.getNode("label").setValue("Engine"); - } else { - # Engines are indexed from 1 in the GUI. - target.getNode("label").setValue("Engine " ~ (engine + 1)); - } + row = row + 1; - # Now the checkbox - target = group.getNode("checkbox[" ~ i ~ "]", 1); - props.copy(group.getChild("checkbox-template"), target); - target.getNode("row").setValue(row); + # Set up the label + target = group.getNode("text[" ~ i ~ "]", 1); + props.copy(group.getNode("text-template"), target); + target.getNode("row").setValue(row); - var failure = "/sim/failure-manager/engines/engine[" ~ engine ~ "]/serviceable"; - target.getNode("property").setValue(failure); - - # Finally the MTBF - target = group.getNode("input[" ~ i ~ "]", 1); - props.copy(group.getChild("input-template"), target); - target.getNode("row").setValue(row); - i += 1; - - var mtbf = "/sim/failure-manager/engines/engine[" ~ engine ~ "]/mtbf"; - target.getNode("property").setValue(mtbf); - engine += 1; + if (size(engines.getChildren("engine")) == 1) { + target.getNode("label").setValue("Engine"); + } else { + # Engines are indexed from 1 in the GUI. + target.getNode("label").setValue("Engine " ~ (engine + 1)); } + + # Now the checkbox + target = group.getNode("checkbox[" ~ i ~ "]", 1); + props.copy(group.getChild("checkbox-template"), target); + target.getNode("row").setValue(row); + + var failure = "/sim/failure-manager/engines/engine[" ~ engine ~ "]/serviceable"; + target.getNode("property").setValue(failure); + + # Finally the MTBF + target = group.getNode("input[" ~ i ~ "]", 1); + props.copy(group.getChild("input-template"), target); + target.getNode("row").setValue(row); + i += 1; + + var mtbf = "/sim/failure-manager/engines/engine[" ~ engine ~ "]/mtbf"; + target.getNode("property").setValue(mtbf); + engine += 1; } @@ -132,7 +136,7 @@ 1 1 - /systems/vacuum/serviceable + /sim/failure-manager/systems/vacuum/serviceable @@ -151,7 +155,7 @@ 2 1 - /systems/static/serviceable + /sim/failure-manager/systems/static/serviceable @@ -170,7 +174,7 @@ 3 1 - /systems/pitot/serviceable + /sim/failure-manager/systems/pitot/serviceable @@ -189,7 +193,7 @@ 4 1 - /systems/electrical/serviceable + /sim/failure-manager/systems/electrical/serviceable @@ -287,13 +291,13 @@ 1 1 - /gear/serviceable + /sim/failure-manager/controls/gear/serviceable 1 2 - /sim/failure-manager/controls/gear/gear-down/mcbf + /sim/failure-manager/controls/gear/mcbf @@ -402,7 +406,7 @@