1
0
Fork 0
fgdata/Aircraft/Generic/Systems/failures.nas
Anton Gomez Alvedro 5df7a784e5 Failure Management Framework (1st milestone)
Replaces existing Nasal/failures.nas script with a programmable failure
manager. The failure manager allows dynammic creation and removal of
failure modes, on demand activation and a flexible set of triggers.
The public interface can be found in Nasal/FailureMgr/public.nas

Aircraft/Generic/Systems/failures.nas provides a library of triggers and
failure actuators ready to use for programming the failure manager.

A compatibility layer is included under
Aircraft/Generic/Systems/compat_failure_modes.nas.
This compatibility layer is currently loaded on startup and programs the
FailureMgr to emulate the former behavior (same set of failure modes and
compatible interface through the property tree).

This first milestone is only intended to replace the failure management
engine underneeth with minimum visible changes, and hopefully no aircraft
breakages. Future milestones will build upon this to add a Canvas based
procedural GUI and example integration on aircrafts.
2014-06-06 21:41:11 -05:00

363 lines
8.9 KiB
Text

# 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();
}
}
};