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中:

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_observer5.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_script5.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中进行添加的:

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

注意这个添加事件的方法需要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中使能自定义事件时,事件名称有错误提示,但运行是正常的:

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

增加一个语法文件,比如customevent.ag,并添加自定义的CUSTOM_TIME_EVENT就行了
###############################################################
# observer
###############################################################
# WsfScriptObserver.cpp
(rule observer-event-type {
CUSTOM_TIME_EVENT
})然后再次打开wizard,能正常显示不报错了,且输入时也会有相应的提示。

7. 后记
从入口函数DispatchEvents来看,afsim的事件分发机制并不是多线程的,即不是让每一个事件处理Execute在单独的线程里面完成,而是顺序执行的。这样设计的原因是:afsim认为所有事件都定义了自己的触发时间,因此每个时间必须严格按照时间顺序进行处理。但这里有个问题是,不同事件处理时长是不一样的,如果每个事件处理时间太长,会造成后面的事件积压,特别是FrameStep仿真,可能会造成每一帧的实际时长超过设定的帧间隔。
除了那些依赖于前面事件的结果的事件需要按顺序进行,还有些事件不一定要等到前面的事件处理完成才能开始。因此其实可以对源码进行改造,为事件添加一个类型:即标识是并行事件还是串行事件。这样可以从事件处理上提升处理性能。
好了,上面即是afsim事件机制的分析,以及自定义事件分发处理,并通过Observer发送到脚本进行回调处理的全部内容。
本教程示例程序已放淘宝:

往期推荐
二次开发_DIS分布式仿真
飞腾D2000麒麟V10国防版下编译
服务端引擎增加fs可控帧步进模式
魔改MapDisplay实现二三维同步

评论