From c108f3b9884b206ee520ae51ccba72ffc1757254 Mon Sep 17 00:00:00 2001 From: Anton Gomez Alvedro Date: Sun, 21 Dec 2014 12:39:52 +0100 Subject: [PATCH] Bugfixes and improvements to the Failure Manager - Fix: runtime exception in remove_failure_mode() - Fix: keep failure & trigger status on teleport. - Fix: allow random failures from the gui to be enabled/disabled multiple times. - Fix: mcbf/mtbf are set to zero when they fire, so they can be reactivated from the gui. - Fix: string casts of several trigger types had syntax errors. - Usability: screen messages related to failures now use positive logic: "condition 100%" instead of "failure level 0%" - Performance: Time triggers now use internal timers, instead of requiring being polled. - Reviewed Trigger interface for more rational usage. reset() is replaced by arm()/disarm() - Added a subscription interface to listen to FailureMgr events. - Added an internal log buffer to keep a record of relevant events and present them to gui elements. - Several usability improvements to the FailureMgr Nasal API. --- .../Generic/Systems/compat_failure_modes.nas | 68 ++++--- Aircraft/Generic/Systems/failures.nas | 100 +++++++--- Nasal/FailureMgr/private.nas | 174 +++++++++++------- Nasal/FailureMgr/public.nas | 147 ++++++++++++--- Nasal/events.nas | 108 +++++++++++ 5 files changed, 445 insertions(+), 152 deletions(-) create mode 100644 Nasal/events.nas diff --git a/Aircraft/Generic/Systems/compat_failure_modes.nas b/Aircraft/Generic/Systems/compat_failure_modes.nas index 1fdfe5645..64992b184 100644 --- a/Aircraft/Generic/Systems/compat_failure_modes.nas +++ b/Aircraft/Generic/Systems/compat_failure_modes.nas @@ -70,32 +70,13 @@ var compat_modes = [ 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); + FailureMgr.set_failure_level(id, value ? 0 : 1); return; } @@ -107,17 +88,33 @@ var compat_listener = func(prop) { # mtbf and mcbf parameter handling var trigger = FailureMgr.get_trigger(id); - if (value == 0) { - trigger != nil and FailureMgr.set_trigger(id, nil); + if (trigger == nil or (trigger.type != "mcbf" and trigger.type != "mtbf")) return; - } - if (trigger == nil) { - FailureMgr.set_trigger(id, new_trigger()); - } - else { - trigger.set_param(name, value); - trigger.reset(); + if (value != 0) + trigger.set_param(name, value) and trigger.arm(); + else + trigger.disarm(); +} + +## +# Listens to FailureMgr events. Resets mcbf/mtbf params to zero so they can +# be rearmed from the GUI. + +var trigger_listener = func(event) { + var trigger = event.trigger; + + # Only control modes in our compat list, i.e. do not interfere + # with custom scripts. + + if (trigger.type != "mtbf" and trigger.type != "mcbf") + return; + + foreach (var m; compat_modes) { + if (m.id == event.mode_id) { + trigger.set_param(trigger.type, 0); + break; + } } } @@ -200,10 +197,21 @@ var compat_setup = func { setlistener(n, compat_listener, 0, 0); setlistener(prop ~ "/failure-level", compat_listener, 0, 0); - var trigger_type = (m.type == MTBF) ? "/mtbf" : "/mcbf"; + if (m.type == MTBF) { + var trigger_type = "/mtbf"; + FailureMgr.set_trigger(m.id, MtbfTrigger.new(0)); + } + else { + var trigger_type = "/mcbf"; + var control = contains(m, "mcbf_prop")? m.mcbf_prop : m.id; + FailureMgr.set_trigger(m.id, McbfTrigger.new(control, 0)); + } + setprop(prop ~ trigger_type, 0); setlistener(prop ~ trigger_type, compat_listener, 0, 0); } + + FailureMgr.events["trigger-fired"].subscribe(trigger_listener); } diff --git a/Aircraft/Generic/Systems/failures.nas b/Aircraft/Generic/Systems/failures.nas index 998539e9c..8e70dd478 100644 --- a/Aircraft/Generic/Systems/failures.nas +++ b/Aircraft/Generic/Systems/failures.nas @@ -121,6 +121,7 @@ var norm_rand = func(mean, std) { var AltitudeTrigger = { parents: [FailureMgr.Trigger], + type: "altitude", requires_polling: 1, new: func(min, max) { @@ -136,9 +137,12 @@ var AltitudeTrigger = { }, 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"])) + var min = me.params["min-altitude-ft"]; + var max = me.params["max-altitude-ft"]; + + if (min == nil) sprintf("Altitude below %d ft", int(max)); + elsif (max == nil) sprintf("Altitude above %d ft", int(min)); + else sprintf("Altitude between %d and %d ft", int(min), int(max)); }, update: func { @@ -159,6 +163,7 @@ var AltitudeTrigger = { var WaypointTrigger = { parents: [FailureMgr.Trigger], + type: "waypoint", requires_polling: 1, new: func(lat, lon, distance) { @@ -174,15 +179,15 @@ var WaypointTrigger = { return m; }, - reset: func { - call(FailureMgr.Trigger.reset, [], me); + arm: func { + call(FailureMgr.Trigger.arm, [], 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)); + geo.format(me.waypoint.lat(), me.waypoint.lon())); }, update: func { @@ -197,27 +202,41 @@ var WaypointTrigger = { var MtbfTrigger = { parents: [FailureMgr.Trigger], - # TODO: make this trigger async - requires_polling: 1, + type: "mtbf", + requires_polling: 0, 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"; + m.timer = maketimer(0, func m.on_fire()); + m.timer.singleShot = 1; 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); + enable: func { + me.armed and me.timer.start(); + me.enabled = 1; + }, + + disable: func { + me.timer.stop(); + me.enabled = 0; + }, + + arm: func { + call(FailureMgr.Trigger.arm, [], me); + me.timer.restart(norm_rand(me.params["mtbf"], me.params["mtbf"] / 10)); + me.enabled and me.timer.start(); + }, + + disarm: func { + call(FailureMgr.Trigger.disarm, [], me); + me.timer.stop(); }, to_str: func { - sprintf("Mean time between failures: %f.1 mins", me.params["mtbf"] / 60); + sprintf("Mean time between failures: %.1f mins", me.params["mtbf"] / 60); }, update: func { @@ -231,22 +250,37 @@ var MtbfTrigger = { var TimeoutTrigger = { parents: [FailureMgr.Trigger], - # TODO: make this trigger async - requires_polling: 1, + type: "timeout", + requires_polling: 0, new: func(timeout) { var m = FailureMgr.Trigger.new(); m.parents = [TimeoutTrigger]; m.params["timeout-sec"] = timeout; - fire_time = 0; + m.timer = maketimer(0, func m.on_fire()); + m.timer.singleShot = 1; 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"]; + enable: func { + me.armed and me.timer.start(); + me.enabled = 1; + }, + + disable: func { + me.timer.stop(); + me.enabled = 0; + }, + + arm: func { + call(FailureMgr.Trigger.arm, [], me); + me.timer.restart(me.params["timeout-sec"]); + me.enabled and me.timer.start(); + }, + + disarm: func { + call(FailureMgr.Trigger.disarm, [], me); + me.timer.stop(); }, to_str: func { @@ -284,7 +318,10 @@ var CycleCounter = { }, disable: func { - if (me._lsnr != nil) removelistener(me._lsnr); + if (me._lsnr != nil) { + removelistener(me._lsnr); + me._lsnr = nil; + } }, reset: func { @@ -319,6 +356,7 @@ var CycleCounter = { var McbfTrigger = { parents: [FailureMgr.Trigger], + type: "mcbf", requires_polling: 0, new: func(property, mcbf) { @@ -327,7 +365,6 @@ var 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; }, @@ -341,8 +378,8 @@ var McbfTrigger = { me.enabled = 0; }, - reset: func { - call(FailureMgr.Trigger.reset, [], me); + arm: func { + call(FailureMgr.Trigger.arm, [], me); me.counter.reset(); me.activation_cycles = norm_rand(me.params["mcbf"], me.params["mcbf"] / 10); @@ -350,14 +387,17 @@ var McbfTrigger = { me.enabled and me.counter.enable(); }, + disarm: func { + call(FailureMgr.Trigger.disarm, [], me); + me.enabled and me.counter.disable(); + }, + 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 index d124acf95..cc397d366 100644 --- a/Nasal/FailureMgr/private.nas +++ b/Nasal/FailureMgr/private.nas @@ -21,6 +21,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + ## # Represents one way things can go wrong, for example "a blown tire". @@ -53,9 +54,7 @@ var FailureMode = { # and 1 total failure. set_failure_level: func(level) { - me._path != nil or - die("FailureMode.set_failure_level: Unbound failure mode"); - + assert(me._path != nil, "FailureMode.set_failure_level: unbound mode"); setprop(me._path ~ me.id ~ "/failure-level", level); }, @@ -64,8 +63,7 @@ var FailureMode = { _set_failure_level: func(level) { me.actuator.set_failure_level(level); - me._log_failure(sprintf("%s failure level %d%%", - me.description, level*100)); + _failmgr.log(sprintf("%s condition %d%%", me.description, (1-level)*100)); }, ## @@ -80,7 +78,7 @@ var FailureMode = { # path/failure-level (double, rw) bind: func(path) { - me._path == nil or die("FailureMode.bind: mode already bound"); + 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"); @@ -95,37 +93,31 @@ var FailureMode = { 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. +# 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: {}, - pollable_trigger_count: 0, + logbuf: events.LogBuffer.new(echo: 1), init: func { me.timer = maketimer(me.update_period, func me._update()); - setlistener("sim/signals/reinit", func me._on_reinit()); + 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"); @@ -133,10 +125,6 @@ var _failmgr = { 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); @@ -145,37 +133,40 @@ var _failmgr = { mode.bind(proproot); }, - ## - # Remove a failure mode from the system. - # id: FailureMode id string, e.g. "systems/pitot" + 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: " ~ mode_id); + 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].unbind(); - props.globals.getNode(proproot ~ id).remove(); + me.failure_modes[id].mode.unbind(); 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. + 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 @@ -191,7 +182,6 @@ var _failmgr = { 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; @@ -200,13 +190,10 @@ var _failmgr = { me.timer.start(); } - trigger.enable(); + if (me.enabled()) + 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); @@ -216,24 +203,32 @@ var _failmgr = { ## # Observer interface. Called from asynchronous triggers when they fire. - # trigger: Reference to the calling trigger. + # 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) { - me.failure_modes[id].mode.set_failure_level(1); found = 1; + me.failure_modes[id].mode.set_failure_level(1); + trigger.disarm(); + FailureMgr.events["trigger-fired"].notify( + { mode_id: id, trigger: trigger }); break; } } - found or die("FailureMgr.on_trigger_activated: trigger not found"); + assert(found, "FailureMgr.on_trigger_activated: trigger not found"); }, ## - # Enable the failure manager. + # 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)) { @@ -248,6 +243,7 @@ var _failmgr = { ## # 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(); @@ -259,13 +255,16 @@ var _failmgr = { }, - ## - # Returns enabled status. - 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. @@ -273,18 +272,23 @@ var _failmgr = { _update: func { foreach (var id; keys(me.failure_modes)) { var failure = me.failure_modes[id]; + var trigger = failure.trigger; - 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); - } + 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 }); } }, - ## - # Detaches a trigger from the system. - _discard_trigger: func(trigger) { trigger.disable(); trigger.unbind(); @@ -296,17 +300,47 @@ var _failmgr = { }, ## - # Reinit listener. Sets all failure modes to "working fine". + # Teleport listener. During repositioning, all triggers are disabled to + # avoid them firing in a possibly inconsistent state. - _on_reinit: func { - foreach (var id; keys(me.failure_modes)) { - var failure = me.failure_modes[id]; + _on_teleport: func(pnode) { - failure.mode.set_failure_level(0); + 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(); + } + } + }, - if (failure.trigger != nil) { - me._discard_trigger(failure.trigger); - failure.trigger = nil; + 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)"); } } } diff --git a/Nasal/FailureMgr/public.nas b/Nasal/FailureMgr/public.nas index 957bb54a5..69bf1c288 100644 --- a/Nasal/FailureMgr/public.nas +++ b/Nasal/FailureMgr/public.nas @@ -20,6 +20,46 @@ var proproot = "sim/failure-manager/"; +## +# Nasal modules can subscribe to FailureMgr events. +# Each event has an independent dispatcher so modules can subscribe only to +# the events they are interested in. This also simplifies processing at client +# side by being able to subscibe different callbacks to different events. +# +# Example: +# +# var handle = FailureMgr.events["trigger-fired"].subscribe(my_callback); + +var events = {}; + +# Event: trigger-fired +# Format: { mode_id: , trigger: } +events["trigger-fired"] = globals.events.EventDispatcher.new(); + +## +# Encodes a pair "category" and "failure_mode" into a "mode_id". +# +# These just have the simple form "category/mode", and are used to refer to +# failure modes throughout the FailureMgr API and to create a path within the +# sim/failure-manager property tree for the failure mode. +# +# examples of categories: +# structural, instrumentation, controls, sensors, etc... +# +# examples of failure modes: +# altimeter, pitot, left-tire, landing-light, etc... + +var get_id = func(category, failure_mode) { + return sprintf("%s/%s", string.normpath(category), failure_mode); +} + +## +# Returns a vector containing: [category, failure_mode] + +var split_id = func(mode_id) { + return [string.normpath(io.dirname(mode_id)), io.basename(mode_id)]; +} + ## # Subscribe a new failure mode to the system. # @@ -38,6 +78,15 @@ var add_failure_mode = func(id, description, actuator) { FailureMode.new(id, description, actuator)); } +## +# Returns a vector with all failure modes in the system. +# Each vector entry is a hash with the following keys: +# { id, description } + +var get_failure_modes = func() { + _failmgr.get_failure_modes(); +} + ## # Remove a failure mode from the system. # id: FailureMode id string, e.g. "systems/pitot" @@ -79,10 +128,36 @@ var get_trigger = func(mode_id) { # 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) { +var set_failure_level = func(mode_id, level) { setprop(proproot ~ mode_id ~ "/failure-level", level); } +## +# Returns the current failure level for the given failure mode. +# mode_id: Failure mode id string. + +var get_failure_level = func(mode_id) { + getprop(proproot ~ mode_id ~ "/failure-level"); +} + +## +# Restores all failure modes to level = 0 + +var repair_all = func { + _failmgr.repair_all(); +} + +## +# Returns a vector of timestamped failure manager events, such as the +# messages shown in the console when there are changes to the failure conditions. +# +# Each entry in the vector has the following format: +# { time: