Emesary: Nasal implementation for FlightGear

I’ve been recently looking at how to improve the way that the F-14 is built after starting to integrate the enhancements that Fabien has made to the radar system.

The trouble is that the F-14 is currently all very interdependent and would massively benefit from being decoupled. This is ideally suited to my standard solution to this sort of problem Emesary

Brief introduction to Emesary

Emesary is a simple and efficient class based interobject communcation system to allow decoupled disparate parts of a system to function together without knowing about each. It allows decoupling and removal of dependencies by using notifications to cause actions or to query values.

Emesary is all about decoupling and removing dependecies, and improving the structure of code. Using Emesary you can more easily define the what rather than the how. By using what is essential an event driven system it is easy to add or remove modules, and also for extra modules to be inserted that the rest of the aircraft knows nothing about (e.g. FGCamera or the Walker).

Emesary is ideally suited to bridge the gap between programming languages in a way that is transparent. I’ve had some C++ code talking to C# (using protobuf); the beauty is that neither side needs to know how the messages are being routed, or indeed even where the messages are coming from or going to.

Emesary allows common systems to be implemented on different aircraft without depdencies. If you wanted to you could build an Arinc 429 bus using Emesary. To say this a different way you can design away the dependencies that you get when referencing different aircraft for common systems.

Emesary is contained within emesary.nas which needs to be in $FGDATA/Nasal

Future developments

HLA

The concepts are very close between Emesary and HLA. The HLA architecture (as I understand it) will permit us to provide a good mapping between messages within an Aircraft model and messages that need to be transmitted to another federate. Emesary brings an efficient structured method for defining messages within a model so it will be a case of figuring out a translation layer to allow these messages to be sent over the wire.

So I have high hopes for what Emesary will enable models to do in the HLA environment.

Multiplayer bridge

There is a multiplayer bridge for Emesary which allows selected messages to be routed to participating aircraft over the multiplayer protocol (MP). The important thing here is that the Bridge will decide which messages are routed, it will enable bi-directional communications between aircraft.

This will uses a string property to send the messages; incoming messages are processed on a per model basis in each MP client. As with all MP it relies on UDP which is not guaranteed transmission so a bridged message will remain in the MP packet for an amount of time to give connected clients a good chance to receive, even so delivery cannot be guaranteed.

Notification Protocols

Emesary works best when a protocol is correctly designed. By protocol I mean simply the order in which messages are transmitted and received. Notifications within an aircraft model can be modified by recipients – which provides an easy way to populate data. However this method is only suitable for notifications that will only ever be sent locally. If a notification may at a future point be sent to an external system it needs to be designed as an asynchronous request with a corresponding response notification.

The request/response protocol works exceptionally well over a distributed system, and the main benefit of the Emesary way of doing this is that the notifications simply appear at the usual place (within the Receive method). The object does not need to know where the notification has come from, and nothing in the system (apart from the Emesary transmitter) needs to know where to send notifications to.

Worked Example

Often I find that a worked example will illustrate something way better than all of the wordy reference section. So this is what I’ve done.

Automatic Carrier Landing System (ACLS)

The US Navy use something like the AN/SPN-46(V) Automatic Carrier Landing System (ACLS). This is a precision approach landing system (PALS) which provides electronic guidance to carrier-based aircraft and allows them to land in all-weather conditions with no limitations due to low ceiling or restricted visibility.

This is basically a set of radars and radio transmitters that allow aircraft equipped with the AN/ARA-63 receiver group to perform an automated landing. Currently this is implemented in the F-14 as a solely aircraft side system that uses the tuned TACAN channel to locate the carrier and performs very simplistic calculations to provide information that allows a precision carrier landing to be performed.

The problem with ACLS and Carriers in FlightGear

The basic problem is that there are (or can be) many Carriers within the AI system, and the aircraft needs to be detected by each of them as it approaches the carrier. A solution would for the aircraft to scan the property tree to locate suitable carriers, however this is introducing a direct dependency between the aircraft and known carriers. This would mean that it would not be possible to have a ground based installation of an AN/SPN-46.

If we use a scan of the AI tree to find the AN/SPN-4x it means that any new AI scenario could well require all aircraft to be modified. This could be avoided by a consistent design and use of the property tree, but the way I’m going to solve the problem with Emesary is more elegant and will work for any future scenarios.

In the F-14 up to V1.3 this problem has been solved in a rather inelegant way by having to tune the Carrier’s TACAN channel, and that way the aircraft can display appropriate data.

The real solution to this problem is to mimic what happens in the real world. Make it possible for each of the carriers to transmit and detect the aircraft. This is explained in the next section, but it’s basically three messages that flow (msg1)Carrier->Aircraft respond with (msg2)->Carrier responds with (msg3)->Aircraft.

The AN/SPN-46 system will decide if an aircraft is in range, however the aircraft will decide if it is tuned into the right channel. An aircraft that isn’t tuned might be visible on a display on the carrier (if we had such a thing), but no guidance would be received back at the aircraft.

AN/SPN 46 implementation

There is a new module Aircraft/Generic/an_spn_46.nas which is a standard implementation of the device; so all that’s needed is for the Vinson.xml model file to instantiate one of these correctly connected to the model, register it with the GlobalTransmitter; and then all aircraft that fly within the correct range will be able to use the AN/SPN-46.

Once instantiated in the carrier the AN/SPN 46 system will send out a ANSPN46ActiveNotification at regular intervals. This will be received by any aircraft registered with emesary.GlobalTransmitter. When the aircraft receives this notification it should respond with a ANSPN46ActiveResponseNotification which indicates to the AN/SPN-46 system whether or not the aircraft is tuned in, and the aircraft position, heading and forwards velocity in feet per second. If the aircraft is tuned to the right channel (notification.IsTuned = true) then a ANSPN46CommunicationNotification response will be sent, again via the GlobalTransmitter that the aircraft can use to display on the appropriate instruments.

Emesary ACLS Message flow

Adding an AN/SPN-46 to a Carrier model

The AI Carrier model needs to load this inside the model XML. It is as simple as adding the Nasal that follows. Things to note are that a timer is used – but that the system itself suggests the frequency. This is because the system will slow down when no aircraft are within range to 0.1 hz (i.e. every 10 seconds).

   <nasal>
        <load>
            <![CDATA[ 
            var self = cmdarg();
            print("Model load Nimitz ", self.getPath());
            fn_net = getprop("/sim/fg-root") ~ "/Aircraft/Generic/an_spn_46.nas";
            io.load_nasal(fn_net, "an_spn_46");
            var anspn = an_spn_46.ANSPN46_System.new("Nimitz", self);
            anspn.SetChannel(2);
            var an_spn_46_timer = maketimer(6, func {
                anspn.Update();
                an_spn_46_timer.restart(anspn.GetUpdateRate());
            });
            an_spn_46_timer.restart(6);
        ]]>
        </load>
        <unload>
            <![CDATA[ 
            an_spn_46_timer.stop();
            print("UNLOAD Nimitz ", self.getPath());
            ]]>
        </unload>
    </nasal>

Adding an ARA-63 receiver to an aircraft

Again this is really simple; most of the work is performed by the Carrier System.

This section responds messages and drives the following lights and the flight director bars.

#
# AN/SPN 46 transmits - this receives.
var ARA63Recipient =
{
    new: func(_ident)
    {
        var new_class = emesary.Recipient.new(_ident);
        new_class.ansn46_expiry = 0;
        new_class.Receive = func(notification)
        {
            if (notification.NotificationType == "ANSPN46ActiveNotification")
            {
                print(" :: Recvd lat=",notification.Position.lat(), " lon=",notification.Position.lon(), " alt=",notification.Position.alt(), " chan=",notification.Channel);
                var response_msg = me.Response.Respond(notification);
#
# We cannot decide if in range as it is the AN/SPN system to decide if we are within range
# However we will tell the AN/SPN system if we are tuned (and powered on)
                if(notification.Channel == getprop("sim/model/f-14b/controls/electrics/ara-63-channel") and getprop("sim/model/f-14b/controls/electrics/ara-63-power-off") == 0)
                    response_msg.Tuned = 1;
                else
                    response_msg.Tuned = 0;
#
# normalised value based on RCS beam power etc.
# we could do this using a factor.
                response_msg.RadarReturnStrength = 1; # possibly response_msg.RadarReturnStrength*RCS_FACTOR
#
                emesary.GlobalTransmitter.NotifyAll(response_msg);
                return emesary.Transmitter.ReceiptStatus_OK;
            }
#---------------------
# we will only receive one of these messages when within range of the carrier (and when the ARA-63 is powered up and has the correct channel set)
#
            else if (notification.NotificationType == "ANSPN46CommunicationNotification")
            {
                me.ansn46_expiry = getprop("/sim/time/elapsed-sec") + 10;
# Use the standard civilian ILS if it is closer.
        print("rcvd ANSPN46CommunicationNotification =",notification.InRange, " dev=",notification.LateralDeviation, ",", notification.VerticalDeviation, " dist=",notification.Distance);
                if(getprop("instrumentation/nav/gs-in-range") and getprop("instrumentation/nav/gs-distance") < notification.Distance)
                {
                    me.ansn46_expiry=0;
                    return emesary.Transmitter.ReceiptStatus_OK;
                }
                else if (notification.InRange)
                {
                    setprop("sim/model/f-14b/instrumentation/nav/gs-in-range", 1);
                    setprop("sim/model/f-14b/instrumentation/nav/gs-needle-deflection-norm",notification.VerticalAdjustmentCommanded);
                    setprop("sim/model/f-14b/instrumentation/nav/heading-needle-deflection-norm",notification.HorizontalAdjustmentCommanded);
                    setprop("sim/model/f-14b/instrumentation/nav/signal-quality-norm",notification.SignalQualityNorm);
                    setprop("sim/model/f-14b/instrumentation/nav/gs-distance", notification.Distance);
                    setprop("sim/model/f-14b/lights/light-10-seconds",notification.TenSeconds);
                    setprop("sim/model/f-14b/lights/light-wave-off",notification.WaveOff);
#
# Set these lights on when in range and within altitude.
# the lights come on but it is unspecified when they go off.
# Ref: F-14AAD-1 Figure 17-4, p17-11 (pdf p685)
                    if (notification.Distance < 11000) 
                    {
                        if (notification.ReturnPosition.alt() > 300 and notification.ReturnPosition.alt() < 425 and abs(notification.LateralDeviation) < 1 )
                        {
                            setprop("sim/model/f-14b/lights/acl-ready-light", 1);
                            setprop("sim/model/f-14b/lights/ap-cplr-light",1);
                        }
                        if (notification.Distance > 8000)  # extinguish at roughly 4.5nm from fix.
                        {
                            setprop("sim/model/f-14b/lights/landing-chk-light", 1);
                        }
                        else
                        {
                            setprop("sim/model/f-14b/lights/landing-chk-light", 0);
                        }
                    }
                }
                else
                {
                    #
                    # Not in range so turn it all off. 
                    # NOTE: Currently this will never be called as the AN/SPN-46 system will not notify us when we are not in range
                    #       It is implemented here for completeness and to do the correct thing if the implemntation changes
                    setprop("sim/model/f-14b/instrumentation/nav/gs-in-range", 0);
                    setprop("sim/model/f-14b/instrumentation/nav/gs-needle-deflection-norm",1);
                    setprop("sim/model/f-14b/instrumentation/nav/heading-needle-deflection-norm",1);
                    setprop("sim/model/f-14b/instrumentation/nav/signal-quality-norm",0);
                    setprop("sim/model/f-14b/instrumentation/nav/gs-distance", -1000000);
                    setprop("sim/model/f-14b/lights/landing-chk-light", 0);
                    setprop("sim/model/f-14b/lights/light-10-seconds",0);
                    setprop("sim/model/f-14b/lights/light-wave-off",0);
                    setprop("sim/model/f-14b/lights/acl-ready-light", 0);
                    setprop("sim/model/f-14b/lights/ap-cplr-light",0);
                }
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        new_class.Response = ANSPN46ActiveResponseNotification.new("ARA-63");
        return new_class;
    },
};

Having made the class we now need to insert the following code to actually instantiate the receiver.

# Instantiate ARA 63 receiver. This will work when approaching any
# carrier that has an active AN/SPN-46 transmitting.
# The ARA-63 is a Precision Approach Landing system that is fitted to all US
# carriers.
var ara63 = ARA63Recipient.new("ARA-63");
emesary.GlobalTransmitter.Register(ara63);

Lastly there needs to be a way to turn everything off when the carrier is detuned or out of range. This requires a seperate method called from the update loop as follows.

#
# Update the ARA-63; this doess two things - firstly to extinguish the
# lights if the validity period expires, and secondly to use the civilian ILS
# if present. This needs to be called by the main aircraft loop
# NOTE: this is necessary because by design the AN/SPN-46 does not transmit
# to receivers that aren't tuned, or when out of range so a method to reset 
# indications is needed.
var ara_63_update = func
{
#
# do not do anything whilst the AN/SPN 46 is within expiry time. 
    if(getprop("/sim/time/elapsed-sec") < ara63.ansn46_expiry)
        return;
#
#  Out of range so set everything off
    setprop("sim/model/f-14b/lights/landing-chk-light", 0);
    setprop("sim/model/f-14b/lights/light-10-seconds",0);
    setprop("sim/model/f-14b/lights/light-wave-off",0);
    setprop("sim/model/f-14b/lights/acl-ready-light", 0);
    setprop("sim/model/f-14b/lights/ap-cplr-light",0);
#
# Ascertain if the civilian ILS is within range and use it if it is. This isn't as per
# the aircraft but IMHO it is reasonable to have this. 
# You will need to tune to the appropriate TACAN channel to get the ILS
    if (getprop("instrumentation/nav/gs-in-range") != nil)
    {
        setprop("sim/model/f-14b/instrumentation/nav/gs-in-range", getprop("instrumentation/nav/gs-in-range"));
        setprop("sim/model/f-14b/instrumentation/nav/gs-needle-deflection-norm",getprop("instrumentation/nav/gs-needle-deflection-norm"));
        setprop("sim/model/f-14b/instrumentation/nav/gs-distance", getprop("instrumentation/nav/gs-distance"));
        setprop("sim/model/f-14b/instrumentation/nav/heading-needle-deflection-norm",getprop("instrumentation/nav/heading-needle-deflection-norm"));
        setprop("sim/model/f-14b/instrumentation/nav/signal-quality-norm",getprop("instrumentation/nav/signal-quality-norm"));
    }
}

Emesary Reference

This is the reference and concepts behind Emesary. Useful to read and understand.

Core concepts

Emesary has Transmitters, Recipients and Notifications.

Transmitters send out Notifications to all of the Recipients that they have registered. A recipient needs to only implement the Receive method and register itself with the transmitter to receive messages.

By convention all recipients must return Transmitter.ReceiptStatus_NotProcessed when they do not process a message.

Transmitter

A transmitter, usually emesary.GlobalTransmitter sends notifications. Each Transmitter must have an Ident.

Recipient

A recipient uses emesary.Recipient as a base class and must provide a Receive method

Notification

A notification must have at least Type and a Value.

The Type is used to identify the message type and to allow recipients to decide if they can process the notification.

The Value is the most important part of the Notification. There can be other properties in the Notification, but the Value is the one that is the most important and as it is common it is the Value that can be made visible in a debug recipient.

Emesary in NASAL

NASAL doesn’t have the rigid structure that you’d find in other object oriented languages, so no interfaces etc. This actually makes the implementation smaller but the core concepts remain the same.

Creating a Transmitter

You can have as many transmitters as you like; however it is usually sufficient to have a single transmitter for all of the Nasal Modules.

For simplicity Emesary instantiates a Transmitter called GlobalTransmitter and referenced via emesary.GlobalTransmitter. The GlobalTransmitter should be used to most implementations, except where there are specialised requirements.

To create a Transmitter you simply do

    var MyTransmitter =  Transmitter.new("MyTransmitter");

Connecting to a Transmitter

There are two ways to connect to a Transmitter; if you have a Nasal class you can implement a Receive method and register with a Transmitter, or you can add a class to your Nasal module that will receive notifications and act upon them.

Connecting to a class

In both of these examples the Recipient is registered with GlobalTransmitter at the time of creation. This is usually what you would want to do, however you don’t need to register during the new method and you can also choose to instantiate your own Transmitter and register with that. Although generally it is better to use the GlobalTransmitter and only create a Transmitter for a good reason.

If you don’t already have a base class then create your new class like this

   new: func(_ident)
    {
        var new_class = emesary.Recipient.new(_ident);
        new_class.Receive = func(notification)
        {
            if (notification.NotificationType == "SomeNotificationType")
            {
                me.count = me.count + 1;
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        emesary.GlobalTransmitter.Register(new_class);
        # the rest
    },

If you already have a base class then use the Recipient construct method to subclass and implement the Receive method.

    new: func(_ident)
    {
        var obj = {parents : [SomeClass] };
#
        emesary.Recipient.construct(_ident, obj);
        obj.Receive = func(notification)
        {
            if (notification.NotificationType == "SomeNotificationType")
            {
                # Do some work
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        emesary.GlobalTransmitter.Register(obj);

Adding a recipient to a module

If you’ve got a module (filename.nas) that just has methods and data then

var MyRecipient =
{
    new: func(_ident)
    {
        var new_class = emesary.Recipient.new(_ident);
        new_class.count = 0;
        new_class.Receive = func(notification)
        {
            if (notification.NotificationType == "SomeNotificationType")
            {
                # Do some work
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        return new_class;
    },
};

ReceiptStatus

The ReceiptStatus return value from the Receive method is an important part of the way that Emesary works.

The rules are

  1. When a notification is not processed by your Receive method you must return emesary.Transmitter.ReceiptStatus_NotProcessed
  2. When a notification is processed succesfully you should return either emesary.Transmitter.ReceiptStatus_OK or emesary.Transmitter.ReceiptStatus_Fail
  3. When you have definitively processed a notification you can return emesary.Transmitter.ReceiptStatus_Finished. A definitive process return will result in no more Recipients being notified.
  4. When you have definitively processed a notification as not possible you can return emesary.Transmitter.ReceiptStatus_Abort. A definitive process return will result in no more Recipients being notified.

A definitive process return will result in no more Recipients being notified. An example of this is when the notification is a request to show the user a message. The message only needs to be shown once so returning emesary.Transmitter.ReceiptStatus_Finished tells Emesary to notify no more recipients.

An example of emesary.Transmitter.ReceiptStatus_Abort would be a request that cannot be fulfilled, for example raising the landing gear whilst on the ground. If you ensure that the first Recipient added to the Transmitter returns Abort when on the ground then it will avoid the need to check this condition in any other Recipients.

Send a notification

To send a Notification simply construct a new Notification (or modify the parameters of an already constructed notification to avoid GC) and call GlobalTransmitter.NotifyAll(notification)

This could look like this

    emesary.GlobalTransmitter.NotifyAll(LandingGearSwitchNotification.new(1));

You can inspect the value of the

NotifyAll return value

Notify all will return a ReceiptStatus that indicates the overall completion status. This is based on the return value from all recipients.

  1. If all Recipients do not process the message the status will be emesary.Transmitter.ReceiptStatus_NotProcessed
  2. If all Recipients return OK the status will be emesary.Transmitter.ReceiptStatus_OK
  3. If at least one Recipient returns Fail the status will be emesary.Transmitter.ReceiptStatus_Fail

The idea behind the return value of NotifyAll is to allow the sender to take an appropriate action; so if the message wasn’t processed at all then maybe show a message, equally if the return value was fail then a differnt message could be shown or maybe a direct action taken.

Example of handling not processed return

    var landing_gear_pos = switch.getValue();
    var lg_switch = LandingGearSwitchNotification.new(landing_gear_pos);
    if (emesary.GlobalTransmitter.NotifyAll(lg_switch) == emesary.Transmitter.ReceiptStatus_NotProcessed)
    {
        # Not processed so do it ourselves
        setprop("/controls/gear/gear-down", lg_switch.Value);
    }

Example of handling fail return

    var landing_gear_pos = switch.getValue();
    var lg_switch = LandingGearSwitchNotification.new(landing_gear_pos);
    if (Transmitter.IsFail(emesary.GlobalTransmitter.NotifyAll(lg_switch)))
    {
        displayMessage("Landing gear cannot be moved");
    }

Difference between Emesary and Listeners

Listeners can be used to achieve the same effect as using Emesary, except using Emesary gives you more control over how the message is handled and is less resource intensive. So if you have a multiple listeners on a property it may be necessary to have similar logic on each element.

The other benefit is decoupling. You can have a standard module that performs certain processing when it receives a notification, but often the property that fires this differs between aircraft models. So in this case you can set a listener that will send a notification via the GlobalTransmitter and the standard module will perform the actions without having to know the property that initiates the action.

Using Notifications to return information

It is possible and often very useful to add extra elements into a Notification that can be filled in by the Recipient. An example of this could be getting the current list of available radar returns for TCAS.

    var RadarReturn =
    {
        new: func(_callsign, _coord, _hasradar)
        {
            var new_class = { parents: [RadarReturn]};
            new_class.Callsign = _callsign;
            new_class.Position = _coord;
            new_class.HasRadar = _hasradar;
            return new_class;
        },
    };
#
    var RadarReturnsNotification =
    {
        new: func(_value)
        {
            var new_class = emesary.Notification.new("RadarReturnNotification", _value);
            new_class.Returns = [];
            return new_class;
        },
        AddReturn: func(radar_return)
        {
            append(me.Returns, radar_return);
        },
    };

and in the Radar processing code

pre. new_class.Receive = func(notification) { if (notification.NotificationType == “RadarReturnsNotification”) { foreach( rr; returns_list ) { notification.AddReturn(rr); # or possibly notification.AddReturn(RadarReturn.new(rr.get_callsign(), rr.get_position(), rr.get_has_radar()); } # Do some work return emesary.Transmitter.ReceiptStatus_Finished; } return emesary.Transmitter.ReceiptStatus_NotProcessed; };

Emesary as a scheduler

Bear in mind that Nasal is not suited to anything related to flight dynamics, and what I’m presenting here as a scheduler is not intended to be used to implement complex dynamics, or at least not until HLA makes it possible to run Nasal code at the same rate as the flight dynamics.

Usually you will have at least one timer inside the Nasal for any given aircraft to perform frequent processing. Sometimes more than one timer is necessary for different rates, and sometimes this will introduce extra dependencies.

Using Emesary you can set a timer that will send out a Notification that will allow all of the registered recipients to perform their required processing. You can obviously send out a less frequent message (for example in only 1 frame out of 4, or every second) for less important processing. The important thing here is that the sending of the notification and therefore the update frequency is only in one place.

So the updateFCS method that usually calls lots of dependent modules can be replaced by the following code.

    var FrameNotification = 
    {
        new: func(_rate)
        {
            var new_class = emesary.Notification.new("FrameNotification", _rate);
            new_class.Rate = _rate;
            new_class.FrameRate = 60;
            new_class.FrameCount = 0;
            new_class.ElapsedSeconds = 0;
            return new_class;
        },
    };
#    
    var frameNotification = FrameNotification.new(1);
#    
    var rtExec_loop = func
    {
        var frame_rate = getprop("/sim/frame-rate");
        var elapsed_seconds = getprop("/sim/time/elapsed-sec");
    #
    # you can put commonly accessed properties inside the message to improve performance.
    #
        frameNotification.FrameRate = frame_rate;
        frameNotification.ElapsedSeconds = elapsed_seconds;
        frameNotification.CurrentIAS = getprop("velocities/airspeed-kt");
        frameNotification.CurrentMach = getprop("velocities/mach");
        frameNotification.CurrentAlt = getprop("position/altitude-ft");
        frameNotification.wow = getprop("gear/gear[1]/wow") or getprop("gear/gear[2]/wow");
        frameNotification.Alpha = getprop("orientation/alpha-indicated-deg");
        frameNotification.Throttle = getprop("controls/engines/engine/throttle");
        frameNotification.e_trim = getprop("controls/flight/elevator-trim");
        frameNotification.deltaT = getprop ("sim/time/delta-sec");
        frameNotification.current_aileron = getprop("surface-positions/left-aileron-pos-norm");
        frameNotification.currentG = getprop ("accelerations/pilot-gdamped");
#    
        if (frameNotification.FrameCount >= 4)
        {
            frameNotification.FrameCount = 0;
        }
        emesary.GlobalTransmitter.NotifyAll(frameNotification);
#    
        frameNotification.FrameCount = frameNotification.FrameCount + 1;
#    
        settimer(rtExec_loop, 0);
    }
    settimer(rtExec_loop, 1);

Then inside each module have a recipient that will receive the appropriate notification and perform the required processing.

For example inside our engines.nas we could do this

    var enginesRecipient = emesary.Recipient.new("Engines");
    enginesRecipient.Receive = func(notification)
    {
        if (notification.NotificationType == "FrameNotification" and notification.FrameCount == 2)
        {
            #print("recv: ",notification.NotificationType, " ", notification.ElapsedSeconds);
            if (APCengaged.getBoolValue())
            {
    		    if ( wow or !getprop("engines/engine[0]/running") or !getprop("engines/engine[1]/running"))
    	            APC_off();
    	    }
            return emesary.Transmitter.ReceiptStatus_OK;
        }
        return emesary.Transmitter.ReceiptStatus_NotProcessed; # we're not processing it, just looking
    }
    emesary.GlobalTransmitter.Register(enginesRecipient);

Troubleshooting

The most common errors are;

  1. Not registering your recipient with the transmitter that is sending the notifications. This is probably the most common mistake #1.
  2. Check that the notification type is correctly spelt
  3. Not returning the appropriate receipt status.
  4. Not sending the right message
  5. Another recipient returning Finished or Aborted when it shouldn’t

Debugging

Debugging is easy; just add a recipient that will print out the contents of the message.

    var debugRecipient = emesary.Recipient.new("Debug");
    debugRecipient.Receive = func(notification)
    {
        if (notification.NotificationType != "FrameNotification")
        {
            print("recv: ",notification.NotificationType, " ", notification.Value);
        }
        return emesary.Transmitter.ReceiptStatus_NotProcessed; # we're not processing it, just looking
    }
    emesary.GlobalTransmitter.Register(debugRecipient);

Other Notes

  • I’ve previously investigated using Emesary instead of the property tree to have notifications whenever discrete items in the simulation change; however whilst this works it generally results in lots of messages being sent every frame. As you can see from the Frame based notification system above it is still possible to transmit a large quantity of data in a notification, the key difference is that a single message with lots of data works better than a lot of discrete messages.
  • Although still a work in progress it should be relatively easy to extend Emesary to route messages using HLA.
  • Design of the notifications helps a lot
  • When communicating bi-directionally between objects you can use either the notification or NotifyAll to cause a second notification to be sent
  • Remember that this is all synchronous. All notifications will be sent and processed within the call to NotifyAll.
AttachmentSize
an_spn_46.nas12.91 KB
emesary.nas6.28 KB
emesary-tests.nas5.39 KB
eisenhower.xml59.33 KB
nimitz.xml74.33 KB
F-14-ARA-63-implementation.nas7.25 KB