1. 说明

自定义事件的分发处理其实早就在学习计划内了,但中间会抛出一些其他想法以及关注的朋友提出的更有趣的想法,所以就一直没有着手弄,借着有朋友提出了这个需求的时候刚好完成了上一篇文章,那就先把这个安排上了^_^。

要注意的是,这个事件跟event_pipe中消息在概念上的差别,event_pipe名字中含有event,但实际上它记录的是事件消息,即事件的发生对仿真世界里各种对象产生影响后输出的状态数据,如MsgPlatformStatus这个记录在event_pipe指定文件中的消息结构如下:

 如上图,它表示的是在simTime仿真时刻,platformIndex对应的平台的状态是什么样的。而本文讨论的事件是:我们应该在此事件发生时做些什么操作来对仿真世界造成影响。一个是果,一个是因!!!

2. 事件分发逻辑

afsim提供了两种仿真模式:FrameStep和EventStep,确切来讲是四种模式,因为每种模式又分为realtime和non-realtime模式,每种模式的简单区分如下:

注:上面并没有考虑设置仿真倍速的情况,若设置了倍速,也只会按相应的倍速来影响实时模式的仿真。

从上面能够看到,不管是帧推进模式还是事件推进模式,最终都会落到事件的分发处理上面(即afsim是事件驱动的仿真)就如源码中的DispatchEvents(aSimTime)调用一样:

 此方法的实现是通过WsfEventManager管理的所有WsfEvent,并根据仿真时间进行调度。具体的调度是在WsfSimulation.cpp中的一个封装方法,通过此方法来触发每个WsfEvent的Execute回调。

而Execute的实现即是各自根据业务进行的。

3. 创建事件

WsfEvent所有事件的基类,由WsfEventManager负责管理,但WsfEventManager并不是单例,而是与单个Simulation对象绑定的,这样设计为不同想定的仿真同步运行提供了框架上的支持。从WsfEvent头文件中可知,我们可以创建两种类型的事件:1、一次性事件;2、可重新调度的事件(内部数据不一定相同)。并且在WsfEvent中也给出了创建这两种类型事件并添加到仿真的具体示例(注:支持C++的Lambda语法的哦^_^):

但要清楚上图的创建方法仅仅是语法糖,关键的部分还是继承WsfEvent或其子类来扩展的自定义的事件,并通过Simulation对象的AddEvent方法添加事件到事件队列的。

4. 处理事件

处理事件有两种层面的概念,一种是自己创建的事件自己处理(自闭环),一种是根据外部发生的事件决定自己该做什么处理。

4.1. 自闭环事件

这种事件说白了就是自己生产自己消费,基本没啥好讲的,看示例就知道怎么玩了,下面我用一个最基本的wsf插件来演示一下这种事件,大致过程如下:SimulationCreated的回调中创建单次触发事件,并设定仿真启动后5秒后触发,单次事件触发后,再创建可重新调度的事件,每5秒触发一次,每次输出事件触发时间。主要代码如下:

void SimulationCreated(WsfSimulation& aSimulation) override
{
    // Simulation对象创建完成后,注册Simulation扩展
    aSimulation.RegisterExtension("wsf_hscustomeventplugin", ut::make_unique<HSCustomEventPlugin>());

    // 初始化创建单次事件
    aSimulation.AddEvent(ut::make_unique<WsfOneShotEvent>(5, [&aSimulation]
        {
            std::cout << "OneShotEvent: " << aSimulation.GetSimTime() << std::endl;
            
            // 在单次事件中创建重新调度事件
            aSimulation.AddEvent(ut::make_unique<WsfRecurringEvent>(5, [&aSimulation](WsfEvent &e)
                {
                    std::cout << "RecurringEvent: " << aSimulation.GetSimTime() << std::endl;
                    e.SetTime(e.GetTime() + 5);
                    return WsfEvent::cRESCHEDULE;
                }));
        }));
}

4.2. 外部事件响应

要响应外部事件就要监听事件,要监听事件就需要知道有哪些事件可以监听,而这些事件又是怎么产生的?怎么与afsim框架产生关系并转发给需求方的?要回答上面的问题,这里以Comm传感器的TurnOn处理来分析下afsim事件是怎么样的运行逻辑。

首先界面触发的位置在warlock的PlatformPartBrowser中:

图片-ofzz.png

1)事件处理

界面上勾选On复选框后TurnOn处理函数的调用堆栈如下图:

这里显示了DispatchEvents的调用,也印证了前面说的afsim的事件处理机制。从上图可知,TurnOn的调用是在WsfPlatformPartEvent的Execute中完成的:

这个partPtr就是附加在平台上的一个发射机接收机部件,这里就调用了TurnOn来打开部件完成了事件的处理。

2)事件创建

上面的TurnOn动作是在WsfPlatformPartEvent事件执行时完成的,而这个事件是在界面勾选On复选框时经过Warlock自行封装的一套Command框架后,会触发到下图所示的函数调用中:

 上图的aState为true,则进入下面TurnPartOn函数,这个是公用函数接口,所有WsfPlatformPart的开机事件都会走这里,里面就通过创建了WsfPlatformPartEvent事件并添加到了Simulation对象中,然后由DispatchEvents来进行分发处理。以上就是基本的事件创建和处理的逻辑,跟自闭环事件差不多,只是这个是afsim内部的东西。

不过还没有回答我们关心的外部事件响应的问题。其实通过上面的流程不难发现,afsim的这套Event事件机制,不具备自动对外通知事件的能力。这也是可以理解的,因为作为对现实的仿真,不应该让某对象产生的事件能够直接告知另外的对象,这样的仿真就会缺少动态感知的能力。

但有一种情况除外:对于某些系统内部的非仿真对象是需要知道仿真对象发生的事件,然后对事件进行处理。比如vtk框架、wkf框架捕捉Comm的TurnOn事件,然后在界面上绘制通信线。这种需求afsim是通过observer来实现的。就如上面的TurnOn处理函数来说,在实现的最后会发出一个Wsf::CommTurnedOn的observer消息:

 Warlock的PlatformPartBroswer中对其进行了监听:

然后在处理函数中更新了界面的勾选状态:

上面就是afsim框架的事件驱动机制。总的来说,事件的创建和处理逻辑比较清晰了,要通知外部对象事件的发生,则只需要发送相关的Observer即可,也可以发送自定义的Observer,而自定义Observer在《event_pipe数据实时接收处理框架》这篇文章中已经有过说明,但本文做了进一步改进,可以在不修改源码的情况下增加自定义Observer。

下面来看看下一个话题:上面的过程都是在C++代码层面实现的,那有没有可能将事件通知到脚本去进行响应呢?

5. 脚本处理事件

我们知道,在脚本中可以定义event_output块来将事件数据记录到指定的文件中,这种情况与event_pipe类似,可以如我之前做event_pipe数据自定义处理那样将所有event数据通过自定义的Observer发送到插件来统一筛选和处理,并在脚本对象类(如platform)中自定义一个脚本回调函数进行回调,但这种方式相对来说要做的工作比较多:定义Observer、开发插件定义脚本回调函数签名、监听事件并调用脚本函数,后面有需要再发文说明。本文主要以另外一种稍微简单但完全够用的方式来让脚本处理事件。

我在搜索event相关的文档时,发现了Observer这个脚本块,查看相关说明后发现这是通过实现脚本签名函数来处理相关事件的:

上图即是observer脚本的说明文档,通过在observer块中使能相应的事件名称,并在脚本中实现脚本签名函数,则能在事件发生时触发实现。

下面以COMM_TURNED_ON事件为例来展示一下内置事件通过observer脚本定义处理的过程:

5.1 在脚本中添加observer,并使能COMM_TURNED_ON事件

// 定义observer
observer
    enable COMM_TURNED_ON
end_observer

5.2 在脚本中根据脚本签名定义回调函数

// 定义COMM_TURNED_ON事件的回调函数
script void CommTurnedOn(WsfComm aComm)
   // 获取平台
   WsfPlatform platform = aComm.Platform();
   int index = platform.Index();
   string name = platform.Name();
   writeln("T = ", TIME_NOW);
   writeln("Index = ", index);
   writeln("Name = ", name);
end_script

5.3 启动warlock,在PlatformPartBrowser中勾选On,可看到控制台输出

 6. 自定义

上面是预定义事件的响应处理,其实内置的事件已经能够满足绝大部分需求了,而afsim也是支持扩展事件的,下面就来怎么扩展事件并让脚本进行响应。

自定义包括两方面,一是自定义事件,二是自定义脚本回调签名。这里设计一个与刚开始的示例相似的流程:

1、在SimulationCreated回调中创建事件,并添加到Simulation对象;设定5秒后事件发生;

2、事件触发后,在控制台输出事件发生时间,并回调脚本签名函数将事件发送到脚本中;

3、脚本处理时也输出事件发生的时间

4、最后Execute返回cRESCHEDULE,表示可重新调度此事件,以后每5秒触发一次此事件

只不过这里是通过继承WsfEvent来实现的,而不是Lambda表达式。

6.1. 自定义事件

在上面搭建的wsf插件中创建一个事件类继承WsfEvent并实现Execute,定义事件的名称为“CUSTOM_TIME_EVENT”

// HSCustomEvent.h
class HSCustomEvent : public WsfEvent
{
public:
static constexpr const char* cNAME = "CUSTOM_TIME_EVENT";

    HSCustomEvent(double aSimTime, int aPriority = 0);

    // 实现Execute纯虚函数
    virtual EventDisposition Execute() override;
};
// HSCustomEvent.cpp
HSCustomEvent::HSCustomEvent(double aSimTime, int aPriority /*= 0*/)
    : WsfEvent(aSimTime, aPriority)
{

}
WsfEvent::EventDisposition HSCustomEvent::Execute()
{
    std::cout << "CustomEvent: " << GetTime() << std::endl;
    // 设置5秒后再次触发
    SetTime(GetTime() + 5);
    // 此返回值表示可重新调度
    return WsfEvent::cRESCHEDULE;
}

6.2. 添加事件到Simulation

自定义事件要添加到Simulation只需要调用它的AddEvent函数即可:

// 5秒后触发自定义事件
aSimulation.AddEvent(ut::make_unique<WsfOneShotEvent>(5, [&aSimulation]
    {
        // 自定义的事件5秒后触发
        aSimulation.AddEvent(std::make_unique<HSCustomEvent>(5));
    }));

编译运行,5秒后就会进入Execute,并且后续每5秒又会再次进入:

6.3. 注册事件回调到脚本环境

注册事件是为了将我们定义的自定义事件名称能够被脚本识别,并且事件发生后能够通知到定义的脚本签名函数,从而触发脚本层面代码逻辑实现。通过查看内置事件的注册过程,可知能通过Observer使能的事件都是在WsfScriptObserver中进行添加的:

图片-eHpv.png

 在WsfScriptObserver中提供了两种添加事件的方法:

图片-Mgon.png

 注意这个添加事件的方法需要Observer作为参数进行传递,因此先来创建Observer,在《event_pipe》文章中我们是通过修改WsfSimulation源码来实现Observer的添加,虽然改动很小,但WsfSimulation作为最基础的类,一旦改动会影响到所有代码。下面采用另外一种方式添加自定义的Observer,首先是定义:

// HSCustomEventObserver.h
namespace WsfObserver
{
    // 返回时间和当前仿真对象
    using CustomTimeEventCallback = UtCallbackListN<void(double, WsfPlatform*, const std::string &)>;

    CustomTimeEventCallback& CustomTimeEvent(const WsfSimulation* aSimulationPtr);
} // namespace WsfObserver
struct HSCustomEventObserver
{
    WsfObserver::CustomTimeEventCallback CustomTimeEvent;
};
// Helper macro for observer objects to implement their callback accessors
#define CUSTOMEVENT_OBSERVER_CALLBACK_DEFINE(OBSERVER, EVENT)                                \
   WsfObserver::EVENT##Callback& WsfObserver::EVENT(const WsfSimulation* aSimulationPtr) \
   {\
        return HSCustomEventPlugin::Find(aSimulationPtr)->Get##OBSERVER##Observer().EVENT;     \
   }
// HSCustomEventObserver.cpp
#include "HSCustomEventObserver.hpp"
#include "HSCustomEventPlugin.hpp"

CUSTOMEVENT_OBSERVER_CALLBACK_DEFINE(HSCustomEvent, CustomTimeEvent)

上面的#define相关的内容就是在不改变WsfSimulation源码的情况下添加Observer,这个只需要在本插件的主类中定义一个Get方法就行了。缺点呢就是这个Observer不是全局的,其他插件要使用或监听则需要链接这个插件,不过跟每次增加Observer都要编译全部代码来说,这已经比较好了。

定义好Observer后,就可以通过WsfScriptObserver来进行注册,此对象可以通过WsfScriptObserver::Find函数进行获取,因此在SimulationCreated回调中来对我们自定义的事件进行注册。

// 注册事件和脚本回调函数到脚本环境
WsfScriptObserver::Find(aSimulation)->AddEvent(
    "CUSTOM_TIME_EVENT",  // 事件名称
    WsfObserver::CustomTimeEvent(&aSimulation),  // 关联的Observer
    "CustomTimeEvent",  // 事件回调函数的脚本签名
    "WsfPlatform, string"  // 事件回调函数的参数
);

然后这里仅做测试,所以直接在自定义事件的Execute中发送这个Observer:

// 通过Observer发送事件
WsfObserver::CustomTimeEvent(GetSimulation())(
    GetTime(),
    GetSimulation()->GetPlatformByIndex(1),  // WsfPlatform参数
    "↑↑↓↓←→←→BA");   // string 参数

6.4. 在脚本observer中使能自定义事件

与上面讲的COMM_TURNED_ON类似,在wizard中使能我们自定义的事件,并实现脚本签名函数:

// 定义observer
observer
    // 使能自定义事件
    enable CUSTOM_TIME_EVENT
end_observer

script void CustomTimeEvent(WsfPlatform platform, string str)
   int index = platform.Index();
   string name = platform.Name();
   writeln("T = ", TIME_NOW);
   writeln("Index = ", index);
   writeln("Name = ", name);
   writeln("Str = ", str);
end_script

然后启动调试,即可在控制台输出中查看到上面的输出信息,表示正常触发了我们定义的脚本事件的实现了。

6.5. ag文件

可能大家注意到wizard中使能自定义事件时,事件名称有错误提示,但运行是正常的:

图片-qumC.png

这个仅仅是因为没有设置语法文件告知wizard,这个自定义事件是可用的。要处理这个问题,也比较简单,参考wsf.ag文件

图片-sbve.png

增加一个语法文件,比如customevent.ag,并添加自定义的CUSTOM_TIME_EVENT就行了

###############################################################
# observer
###############################################################

# WsfScriptObserver.cpp
(rule observer-event-type {
   CUSTOM_TIME_EVENT
})

然后再次打开wizard,能正常显示不报错了,且输入时也会有相应的提示。

图片-Evtd.png

7. 后记

从入口函数DispatchEvents来看,afsim的事件分发机制并不是多线程的,即不是让每一个事件处理Execute在单独的线程里面完成,而是顺序执行的。这样设计的原因是:afsim认为所有事件都定义了自己的触发时间,因此每个时间必须严格按照时间顺序进行处理。但这里有个问题是,不同事件处理时长是不一样的,如果每个事件处理时间太长,会造成后面的事件积压,特别是FrameStep仿真,可能会造成每一帧的实际时长超过设定的帧间隔。

除了那些依赖于前面事件的结果的事件需要按顺序进行,还有些事件不一定要等到前面的事件处理完成才能开始。因此其实可以对源码进行改造,为事件添加一个类型:即标识是并行事件还是串行事件。这样可以从事件处理上提升处理性能。

好了,上面即是afsim事件机制的分析,以及自定义事件分发处理,并通过Observer发送到脚本进行回调处理的全部内容。

本教程示例程序已放淘宝:

往期推荐

二次开发_DIS分布式仿真

飞腾D2000麒麟V10国防版下编译

服务端引擎增加fs可控帧步进模式

魔改MapDisplay实现二三维同步

文末二维码.png