#--------------------------------------------------------------------------- # # 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(3,"(",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(4," ",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 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, lp : aircraft.lowpass.new(3), frameNotification : FrameNotification.new(1, transmitter), frame_inc : 0, cur_frame_inc : 0.033, # start off at 33hz }; new_class.transmitter = transmitter; # 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", }; 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; }, start : func { me.execTimer.start(); }, stop : func { me.execTimer.stop(); }, # 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; me.frameNotification.filtered_frame_rate_worst = (int)(me.lp.filter(me.frameNotification.frame_rate_worst)); # 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. if (me.frameNotification.filtered_frame_rate_worst < 5) { me.frame_inc = 0.25;#4 Hz } elsif (me.frameNotification.filtered_frame_rate_worst < 10) { me.frame_inc = 0.125;#8 Hz } elsif (me.frameNotification.filtered_frame_rate_worst < 15) { me.frame_inc = 0.10;#10 Hz } elsif (me.frameNotification.filtered_frame_rate_worst < 20) { me.frame_inc = 0.075;#13.3 Hz } elsif (me.frameNotification.filtered_frame_rate_worst < 25) { me.frame_inc = 0.05;#20 Hz } elsif (me.frameNotification.filtered_frame_rate_worst < 40) { me.frame_inc = 0.0333;#30 Hz } else { me.frame_inc = 0.02;#50 Hz } # Adjust timer if new value if (me.frame_inc != me.cur_frame_inc) { logprint(3, me.Ident~": Adjust frequency to ",1/me.frame_inc, " Hz"); me.cur_frame_inc = 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();