aeae504d47
Change to calculate the frame rate ourselves because /sim/frame-rate doesn't take into account freeze and has a transient ridiculous value after a pause. Instead we calculate the average rate over a period and then LP filter this. This also removes the annoying frame rate update messages Cofiguration now comes from /sim/emexec * /sim/emexec/monitor-period is the period to reset the average; the LP filter isn't reset. * /sim/emexec/max-rate-hz is the upper limit on the emexec update rate. defaults to 50 and a model can override this. It's probably not a user setting output of * current emexec rate into /sim/emexec/rate-hz * smoothed / LP filtered frame rate into /sim/emexec/frame-rate
340 lines
No EOL
14 KiB
Text
340 lines
No EOL
14 KiB
Text
#---------------------------------------------------------------------------
|
|
#
|
|
# Title : Emesary based 'real time' module executive
|
|
#
|
|
# File Type : Implementation File
|
|
#
|
|
# Description : Uses Emesary notifications to permit Nasal subsystems to be invoked in
|
|
# : a controlled manner.
|
|
# :
|
|
# : Sends out a FrameNotification for each frame recipient can implement
|
|
# : workload reduction as appropriate based on skipping frames (2=half,
|
|
# : 4=quarter etc.) because some code can safely be run at quarter rate
|
|
# : (e.g. ~10hz).
|
|
# :
|
|
# : The developer should interleave slower rate modules to spread out
|
|
# : workload A frame is defined by the timer rate; which is usually the
|
|
# : maximum rate as determined by the FPS.
|
|
# :
|
|
# : This is an alternative to the timer based or explicit function calling
|
|
# : way of invoking aircraft systems. It has the advantage of using less
|
|
# : timers and remaining modular, as each aircraft subsytem can simply
|
|
# : register itself with the global transmitter to receive the frame
|
|
# : notification.
|
|
#
|
|
# See Also : https://wiki.flightgear.org/Nasal_Optimisation#Emesary_real_time_executive
|
|
# : F-15 and F-14 for examples of how to use this.
|
|
#
|
|
# Author : Richard Harrison (richard@zaretto.com)
|
|
#
|
|
# Creation Date : 4 June 2018
|
|
#
|
|
# Copyright (C) 2018 Richard Harrison Released under GPL V2
|
|
#
|
|
#---------------------------------------------------------------------------*/
|
|
|
|
#
|
|
#
|
|
# This is the notification that is sent out to all recipients each frame.
|
|
# The notification contains a hash of property values.
|
|
# Frame modules can request that the hash includes key/property pairs
|
|
# by using the FrameNotificationAddProperty
|
|
#
|
|
# An instance of this class is be contained within the EmesaryExecutive.
|
|
var FrameNotification =
|
|
{
|
|
debug: 0,
|
|
# The rate and the transmitter to use
|
|
new: func(_rate, transmitter=nil)
|
|
{
|
|
if (transmitter == nil)
|
|
transmitter = emesary.GlobalTransmitter;
|
|
|
|
var new_class = emesary.Notification.new("FrameNotification", _rate, 0);
|
|
append(new_class.parents, FrameNotification);
|
|
new_class.Rate = _rate;
|
|
new_class.FrameCount = 0;
|
|
new_class.ElapsedSeconds = 0;
|
|
new_class.monitored = {};
|
|
new_class.properties = {};
|
|
new_class.transmitter = transmitter;
|
|
|
|
#
|
|
# embed a recipient within this notification to allow the monitored property
|
|
# mapping list to be modified.
|
|
new_class.Recipient = emesary.Recipient.new("FrameNotification");
|
|
new_class.Recipient.Receive = func(notification)
|
|
{
|
|
if (notification.NotificationType == "FrameNotificationAddProperty")
|
|
{
|
|
var root_node = props.globals;
|
|
if (notification.root_node != nil) {
|
|
root_node = notification.root_node;
|
|
}
|
|
if (new_class.properties[notification.property] != nil
|
|
and new_class.properties[notification.property] != notification.variable)
|
|
logprint(1,"FrameNotification: (",notification.module,") FrameNotification: already have variable ",new_class.properties[notification.property]," for ",notification.variable, " referencing property ",notification.property);
|
|
|
|
if (new_class.monitored[notification.variable] != nil
|
|
and new_class.monitored[notification.variable].getPath() != notification.property
|
|
and new_class.monitored[notification.variable].getPath() != "/"~notification.property)
|
|
logprint(1,"FrameNotification: (",notification.module,") FrameNotification: already have variable ",notification.variable,"=",new_class.monitored[notification.variable].getPath(), " using different property ",notification.property);
|
|
# else if (new_class.monitored[notification.variable] == nil)
|
|
# print("[INFO]: (",notification.module,") FrameNotification.",notification.variable, " = ",notification.property);
|
|
|
|
new_class.monitored[notification.variable] = root_node.getNode(notification.property,1);
|
|
new_class.properties[notification.property] = notification.variable;
|
|
|
|
logprint(4,"(",notification.module,") FrameNotification.",notification.variable, " = ",notification.property, " -> ", new_class.monitored[notification.variable].getPath() );
|
|
return emesary.Transmitter.ReceiptStatus_OK;
|
|
}
|
|
return emesary.Transmitter.ReceiptStatus_NotProcessed;
|
|
};
|
|
new_class.transmitter.Register(new_class.Recipient);
|
|
return new_class;
|
|
},
|
|
fetchvars : func() {
|
|
foreach (var mp; keys(me.monitored)){
|
|
if(me.monitored[mp] != nil){
|
|
if (FrameNotification.debug > 1)
|
|
logprint(5," ",mp, " = ",me.monitored[mp].getValue());
|
|
me[mp] = me.monitored[mp].getValue();
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
#
|
|
# request to add a property to the frame notification
|
|
var FrameNotificationAddProperty =
|
|
{
|
|
new: func(module, variable, property, root_node=nil)
|
|
{
|
|
var new_class = emesary.Notification.new("FrameNotificationAddProperty", variable, 0);
|
|
if (root_node == nil)
|
|
root_node = props.globals;
|
|
new_class.module = module ;
|
|
new_class.variable = variable;
|
|
new_class.property = property;
|
|
new_class.root_node = root_node;
|
|
return new_class;
|
|
},
|
|
};
|
|
|
|
#
|
|
# The way that the core measures frame rate gives invalid values after a pause so instead
|
|
# we will measure specific to our needs.
|
|
var PerformanceMeasurement = {
|
|
new : func {
|
|
return {
|
|
parents: [PerformanceMeasurement],
|
|
lp : aircraft.lowpass.new(0.1),
|
|
period : props.globals.getNode("/sim/emexec/monitor-period",1),
|
|
frame_count_node : props.globals.getNode("/sim/frame-number"),
|
|
frame_count : getprop("/sim/frame-number") or 0,
|
|
emexec_frame_rate_node : props.globals.getNode("/sim/emexec/frame-rate",1),
|
|
start_time : systime(),
|
|
frame_rate : 30,
|
|
};
|
|
},
|
|
update: func {
|
|
# calculate the average (mean) rate for the monitored period and then let this
|
|
# filter into our output frame rate
|
|
me.delta_seconds = systime() - me.start_time;
|
|
me.frame_count_delta = me.frame_count_node.getValue() - me.frame_count;
|
|
me.frame_rate = me.frame_count_delta / me.delta_seconds;
|
|
me.elapsed_seconds= systime();
|
|
me.emexec_frame_rate_node.setDoubleValue((int)(me.lp.filter(me.frame_rate)));
|
|
|
|
# reset at the end of the defined period as over a long period the average isn't as relevant for our purposes
|
|
if (me.delta_seconds > me.period.getValue()){
|
|
me.start_time = systime();
|
|
me.frame_count = me.frame_count_node.getValue();
|
|
}
|
|
},
|
|
start : func {
|
|
me.timer = maketimer(2, me, func { me.update() });
|
|
me.timer.simulatedTime = 0;
|
|
me.timer.start();
|
|
return me;
|
|
}
|
|
};
|
|
|
|
performanceMeasurement = PerformanceMeasurement.new();
|
|
|
|
#
|
|
# the main exeuctive class.
|
|
# There will be one of these as emexec.ExceModule however multiple instances could be
|
|
# created - but only by those who understand scheduling - because it is not necessary
|
|
# to have more than one - unless we mange to enable some sort of per core threading.
|
|
var EmesaryExecutive = {
|
|
new : func(_ident="EMEXEC", transmitter=nil) {
|
|
|
|
# by default use global transmitter
|
|
if (transmitter == nil)
|
|
transmitter = emesary.GlobalTransmitter;
|
|
|
|
var new_class = {
|
|
parents: [EmesaryExecutive],
|
|
Ident: _ident,
|
|
frameNotification : FrameNotification.new(1, transmitter),
|
|
emexecRate : props.globals.getNode("/sim/emexec/rate-hz",1),
|
|
emexecMaxRate : props.globals.getNode("/sim/emexec/max-rate-hz",1),
|
|
frame_inc : 0,
|
|
cur_frame_inc : 0.033, # start off at 33hz
|
|
};
|
|
new_class.transmitter = transmitter;
|
|
new_class.set_rate(30);
|
|
|
|
# setup the properties to monitor for this system
|
|
var exec_prop_list = {
|
|
frame_rate : "/sim/frame-rate",
|
|
frame_rate_worst : "/sim/frame-rate-worst",
|
|
elapsed_seconds : "/sim/time/elapsed-sec",
|
|
eframe_rate : "/sim/emexec/frame-rate",
|
|
};
|
|
new_class.monitor_properties(exec_prop_list);
|
|
|
|
# now setup the timer.
|
|
# - initially use update rate of 30hz.
|
|
# - use simulated time as otherwise will continue to be called when paused
|
|
# - start timer now, as the listener will effectively block module calls until sim init
|
|
new_class.frameNotification.running = 0;
|
|
setlistener("sim/signals/fdm-initialized", func(v) {
|
|
logprint(1,"started ",new_class.Ident);
|
|
new_class.frameNotification.dT = 0; # seconds
|
|
new_class.frameNotification.curT = 0;
|
|
new_class.frameNotification.running = 1;
|
|
});
|
|
new_class.execTimer = maketimer(new_class.cur_frame_inc, new_class, new_class.timerCallback);
|
|
new_class.execTimer.simulatedTime = 1;
|
|
|
|
return new_class;
|
|
},
|
|
set_rate : func(ratehz){
|
|
me.emexecRate.setValue(ratehz);
|
|
me.cur_frame_inc = 1.0/ratehz;
|
|
me.frame_inc = me.cur_frame_inc;
|
|
},
|
|
start : func {
|
|
me.execTimer.start();
|
|
performanceMeasurement.start();
|
|
},
|
|
stop : func {
|
|
me.execTimer.stop();
|
|
performanceMeasurement.start();
|
|
},
|
|
# request monitoring of a list of hash value pairs.
|
|
monitor_properties : func(input){
|
|
# this uses a notification to isolate the implementation which is also in this module; so it could
|
|
# call directly; however the design is that a FrameNotification add property could also trigger other
|
|
# logic that we do not know about.
|
|
foreach (var name; keys(input)) {
|
|
me.transmitter.NotifyAll(FrameNotificationAddProperty.new(me.Ident, name, input[name]));
|
|
}
|
|
},
|
|
|
|
# ident: String (e.g F-15 HUD)
|
|
# inputs: hash of properties to monitor
|
|
# : e.g
|
|
# {
|
|
# AirspeedIndicatorIndicatedMach : "instrumentation/airspeed-indicator/indicated-mach",
|
|
# Alpha : "orientation/alpha-indicated-deg",
|
|
# }
|
|
# : object - must have an update(notification) method that will receive a frame notification
|
|
# : rate is the frame skip update rate (1/update rate). 0 or 1 means full rate
|
|
# : offset is the offset to permit interleave
|
|
# - e.g for two objects to interleave we could have a rate of two and an offset of 0 and 1 which
|
|
# would result in one object being processed per frame
|
|
register: func(ident, properties_to_monitor, object, rate=1, frame_offset=0) {
|
|
var new_class = emesary.Recipient.new(ident);
|
|
|
|
me.monitor_properties(properties_to_monitor);
|
|
new_class.object = object;
|
|
new_class.Receive = func(notification)
|
|
{
|
|
if (notification.NotificationType == "FrameNotification"){
|
|
if (rate <= 1 or 0 == math.mod(notification.FrameCount + frame_offset,rate)){
|
|
new_class.object.update(notification);
|
|
}
|
|
return emesary.Transmitter.ReceiptStatus_OK;
|
|
}
|
|
return emesary.Transmitter.ReceiptStatus_NotProcessed;
|
|
};
|
|
me.transmitter.Register(new_class);
|
|
return new_class;
|
|
},
|
|
timerCallback : func {
|
|
|
|
if (!me.frameNotification.running){
|
|
logprint(3, me.Ident~": Waiting for start");
|
|
return;
|
|
}
|
|
me.frameNotification.fetchvars();
|
|
me.frameNotification.dT = me.frameNotification.elapsed_seconds - me.frameNotification.curT;
|
|
|
|
if (me.frameNotification.dT > 1.0)
|
|
me.frameNotification.curT = me.frameNotification.elapsed_seconds;
|
|
|
|
me.transmitter.NotifyAll(me.frameNotification);
|
|
me.frameNotification.FrameCount = me.frameNotification.FrameCount + 1;
|
|
|
|
# this permits us to go up to 1/32 rate (which could be less than 1hz)
|
|
if (me.frameNotification.FrameCount > 32) {
|
|
me.frameNotification.FrameCount = 0;
|
|
|
|
# adjust exec rate based on frame rate.
|
|
# calculate exec update rate based on frame rate; this a quadratic function from a curve fit.
|
|
me.frame_inc = (math.round((0.33227017+0.10041432*me.frameNotification.eframe_rate+0.01681707*me.frameNotification.eframe_rate*me.frameNotification.eframe_rate)/5)*5+5);
|
|
|
|
# limit to: 1 <= update rate <= maxRate (default 50)
|
|
me.frame_inc = math.max(1,math.min(me.emexecMaxRate.getValue(), me.frame_inc));
|
|
me.frame_inc = 1/me.frame_inc;
|
|
|
|
# Adjust timer if new value
|
|
if (me.frame_inc != me.cur_frame_inc) {
|
|
me.set_rate(1.0/me.frame_inc);
|
|
me.execTimer.restart(me.cur_frame_inc);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
# profiling aid: embed within a class
|
|
# and each frame call log("something") to trace time
|
|
# e.g. to create using the default log level of INFO:
|
|
# ot = OperationTimer.new("VSD");
|
|
# or for log level debug
|
|
# ot = OperationTimer.new("VSD",2);
|
|
# ...
|
|
# ot.reset();
|
|
# ot.log("start");
|
|
# ... code ...
|
|
# ot.log("half way");
|
|
# ... code ...
|
|
# ot.log("finished");
|
|
|
|
OperationTimer = {
|
|
new : func (ident="timer", level=3) {
|
|
{
|
|
parents: [OperationTimer],
|
|
timestamp: maketimestamp(),
|
|
ident: ident,
|
|
resolution_uS: 1000.0,
|
|
level : level,
|
|
}
|
|
},
|
|
log : func( text){
|
|
logprint(me.level, sprintf("%10s: %8.3f : %s",me.ident, me.timestamp.elapsedUSec()/me.resolution_uS, text));
|
|
},
|
|
reset : func {
|
|
me.timestamp.stamp();
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
var xmit = emesary.Transmitter.new("exec");
|
|
var ExecModule = EmesaryExecutive.new("EMEXEC", xmit);
|
|
ExecModule.start(); |