# 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) { assert(me._path != nil, "FailureMode.set_failure_level: unbound 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); _failmgr.log(sprintf("%s condition %d%%", me.description, (1-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) { assert(me._path == nil, "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; }, }; ## # 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. # Additionally, it also serves to isolate implementation details into its own # namespace. var _failmgr = { pollable_trigger_count: 0, enable_after_teleport: 0, timer: nil, update_period: 10, # 0.1 Hz failure_modes: {}, logbuf: events.LogBuffer.new(echo: 1), init: func { me.timer = maketimer(me.update_period, func me._update()); setlistener("sim/signals/reinit", func(n) me._on_teleport(n)); setlistener("sim/signals/fdm-initialized", func(n) me._on_teleport(n)); 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() }); }, 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); }, get_failure_modes: func { var modes = []; foreach (var k; keys(me.failure_modes)) { var m = me.failure_modes[k]; append(modes, { id: k, description: m.mode.description }); } return modes; }, remove_failure_mode: func(id) { contains(me.failure_modes, id) or die("remove_failure_mode: failure mode does not exist: " ~ id); var trigger = me.failure_modes[id].trigger; if (trigger != nil) me._discard_trigger(trigger); me.failure_modes[id].mode.unbind(); delete(me.failure_modes, id); }, remove_all: func { foreach(var id; keys(me.failure_modes)) me.remove_failure_mode(id); }, repair_all: func { foreach(var id; keys(me.failure_modes)) me.failure_modes[id].mode.set_failure_level(0); }, 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); if (trigger.requires_polling) { me.pollable_trigger_count += 1; if (me.enabled() and !me.timer.isRunning) me.timer.start(); } if (me.enabled()) trigger.enable(); }, 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 firing trigger. on_trigger_activated: func(trigger) { assert(me.enabled(), "A " ~ trigger.type ~ " trigger fired while the FailureMgr was disabled"); var found = 0; foreach (var id; keys(me.failure_modes)) { if (me.failure_modes[id].trigger == trigger) { found = 1; me.failure_modes[id].mode.set_failure_level(1); trigger.disarm(); FailureMgr.events["trigger-fired"].notify( { mode_id: id, trigger: trigger }); break; } } assert(found, "FailureMgr.on_trigger_activated: trigger not found"); }, ## # Enable the failure manager. Starts the trigger poll timer and enables # all triggers. # # Called from /sim/failure-manager/enabled and during a teleport if the # FM was enabled when the teleport was initiated. _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. # Called from /sim/failure-manager/enabled and during a teleport. _disable: func { me.timer.stop(); foreach(var id; keys(me.failure_modes)) { var trigger = me.failure_modes[id].trigger; trigger != nil and trigger.disable(); } }, enabled: func { getprop(proproot ~ "enabled"); }, log: func(message) { me.logbuf.push(message); if (getprop(proproot ~ "/display-on-screen")) screen.log.write(message, 1.0, 0.0, 0.0); }, ## # 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]; var trigger = failure.trigger; if (trigger == nil or !trigger.requires_polling or !trigger.armed) continue; var level = trigger.update(); if (level == 0) continue; if (level != failure.mode.get_failure_level()) failure.mode.set_failure_level(level); trigger.disarm(); FailureMgr.events["trigger-fired"].notify( { mode_id: id, trigger: trigger }); } }, _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(); } }, ## # Teleport listener. During repositioning, all triggers are disabled to # avoid them firing in a possibly inconsistent state. _on_teleport: func(pnode) { if (pnode.getName() == "fdm-initialized") { if (me.enable_after_teleport) { me._enable(); me.enable_after_teleport = 0; } } else { # then, it's /sim/signals/reinit # only react when the signal raises to true. if (pnode.getValue() == 1) { me.enable_after_teleport = me.enabled(); me._disable(); } } }, dump_status: func(mode_ids=nil) { if (mode_ids == nil) mode_ids = keys(me.failure_modes); print("\nFailureMgr Status\n----------------------------------------"); foreach(var id; mode_ids) { var mode = me.failure_modes[id].mode; var trigger = me.failure_modes[id].trigger; print(id, ": failure level ", mode.get_failure_level()); if (trigger == nil) { print(" no trigger"); } else { print(" ", trigger.type, " trigger (", trigger.enabled? "enabled, " : "disabled, ", trigger.armed? "armed)" : "disarmed)"); } } } }; ## # 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);