乐于分享
好东西不私藏

AFSim_二次开发_基于wsf插件扩展内置platform

AFSim_二次开发_基于wsf插件扩展内置platform

基于wsf插件扩展内置platform

1

 前言

本文是对内置平台类型WsfPlatform的扩展,其他如sensor/comm/weapon在官方的PPT中有相应的构建说明,大家查阅即可
在《雷达传感器计算流程解析》文章中曾提到过,WsfScenario的ProcessInputP方法是用于解析脚本文件创建并初始化脚本中定义的各种数据,包括类型定义、平台、系统日志、环境、大气、脚本管理器等等,而平台创建并初始化的入口方法就是里面的LoadPlatformInstance。之前我们也分析过这个方法的处理流程,但当时只是分析平台上的传感器组件的创建过程来了解雷达传感器在什么时候创建并初始化的,跳过了平台的创建过程。
本文是为了扩展内置的platform,那么就需要往前一点来分析platform的创建和初始化过程,然后寻找切入点来创建并初始化我们自定义扩展的pltform。也可以参考afsim的training中的sensor这篇PPT的流程来了解内置类型的扩展流程。

2

 意义

其实不只是需要扩展平台,其他如传感器、通信设备、武器、弹道等模型和算法都需要进行扩展,目的是将这些东西进行隐藏。如果基于afsim的脚本来编写所模型和算法,那么无异于直接将秘密公之于众,这对于技术分享和交流是再好不过了,但对于企业发展及国家安全是不利的,也是不允许的。封装才能获得效益和安全!!!

先看看效果:

    1. 工程展示:源码,语法文件

    2. 编译输出到wsf_plugin,grammar

    3. wizard脚本:扩展类型,扩展属性和单位,script处理输出

    4. 调试warlock,查看ProcessInput处理

    5. 查看控制台,调试自定义脚本接口

已关注

关注

重播 分享

3

 平台创建过程

下面先来分析platform的创建过程吧。首先LoadPlatformInstance方法内部获取定义的platform名称

获取到名称后,AFSim通过一个延迟加载对象WsfDeferredInput来缓存,等其他对象依赖此对象时,或所有其他对象都创建并完成后再来将标记为延迟加载的对象进行创建。

这里先不分析延迟加载的逻辑,直接跳到下方创建平台的代码处。

这里的步骤是:

1)从脚本获取平台的类型

脚本定义平台的语法为:platform [name] [platform_type],其中name为平台的名称,platform_type为此平台的类型。使用simple_scenario.txt场景获取的baseType名称为BLUE_STRIKER,也就是platform直接使用的platform_type,BLUE_STRIKER的继承关系如下图:

2)根据类型克隆创建平台
然后调用GetPlatformTypes方法来获取mPlatformTypesPtr对象,并调用Clone生成基于baseType的platform。这里的WsfPlatformTypes指针是在WsfScenario的CreateTypeLists方法中通过CreateTypeList模板方法创建的

WsfScenario的CreateTypeLists方法是在它本身的构造函数中调用的。查看代码,发现这里面会创建场景中几乎所有的类型列表对象。

CreateTypeList模板方法中的逻辑很简单,就是创建一个模板参数对象,并放入mAllTypeLists列表里而已,那么实际的类型列表对象是在指定的模板参数类实现代码中,比如WsfPlatformTypes平台类型实现类
从上图可以看出,WsfPlatformTypes类型初始化时,将WSF_PLATFORM这种内置类型绑定为WsfPlatfom实现类,即如果脚本定义platform直接指定为WSF_PLATFORM是,对应的实现类就是WsfPlatform。再如下图的WsfSensorTypes的构造函数构建了内置的各种传感器类型:
回到我们的测试脚本txt中,上面我们画出了BLUE_STRIKER的继承图,从脚本解析的platform的bastType为BLUE_STRIKER,那么在创建这个平台是通过BLUE_STRIKER这个类型去WsfPlatformTypes中克隆创建的。因此在WsfPlatformTypes中肯定保存了字符串BLUE_STRIKER与WsfPlatfom对象的映射关系。这个映射关系是在WsfPlatformTypes的LoadType方法中确定并保存到基类WsfObjectTypeListBase的mTypeMap变量中的。

这里就会解析脚本定义的继承链上的所有platform_type并保存,其中就包括BLUE_STRIKER与WsfPlatfom的映射关系。

3)赋予创建的平台唯一标识

上面通过克隆platform_type定义的对象,不具有唯一性,这一步就是为创建的平台赋值一个唯一Id,标识唯一性

4)通过新建的平台处理脚本输入
创建后的平台通过解析脚本文件中定义的属性和值,来初始化对象
5)将新建的平台添加到Scenario
最后将平台添加到场景中用于后续仿真。
通过上面分析,其实应该能明白,形如平台、传感器、通信设备等都是通过上述方式进行创建的。且所有的类型对象的创建都是在WsfScenario的CreateTypeLists方法中,那么我们要想扩展自定义的模型,就需要想办法将我们自定义的类型添加到WsfPlatformTypes类型表中,这可以通过WsfPlatformTypes提供的Add方法来实现。
好了,我们现在就有了实现思路,下面从0开始搭建我们自定义扩展的platform插件。

4

 扩展插件

参考sensor扩展插件的创建方式,我们要对platform进行扩展,那么就不是仅在wizard、warlock或mystic中使用,因此需要对核心wsf框架进行扩展。afsim有三个层面的扩展方式:

    1. WsfScenarioExtension – 如果需要添加新的scenario脚本指令,则需继承此类,并为新的脚本指令提供ProcessInput解析

    2. WsfSimulationExtension – 如果要访问simulation的扩展则需要继承此类,如添加simulation的执行状态的监听

    3. WsfApplicationExtension – 将创建新脚本类型或使用simulation扩展或scenario扩展,需要继承此类

因此要扩展platform,并提供新的脚本指令,就需要对WsfApplicationExtension、WsfScenarioExtension进行继承并实现,下面来依次实现相关内容:

1)wsf插件工程
首先创建一个dll工程(这里如果写cmake并与afsim一起编译应该更好!!),并将输出路径修改到bin目录下的wsf_plugins目录,然后在这个插件项目中添加以下文件:
ExtendPlatform.hppExtendPlatform.cppPluginRegistration.cpp

同时,为了扩展脚本语法文件,还需添加一个extend_platform.ag文件(这个文件要最终放到bin下的所有gammer文件夹内)

2)插件注册

插件注册类代码可以参考《创建AFSim开发环境》这篇文章来配置依赖库和插件注册,这里就不多说了。然后重写WsfApplicationExtension的AddedToApplicationScenarioCreated方法,将我们自定义的脚本接口类和扩展的ExtendPlatform类注册到afsim环境中,代码如下:

// PluginRegistration.cpp#include"wsfplugin_export.h"#include"ExtendPlatform.h"// wsf#include"WsfPlugin.hpp"#include"WsfPlatformTypes.hpp"#include"WsfApplicationExtension.hpp"class ExtendPlatformApplicationExtension : public WsfApplicationExtension{public:    ExtendPlatformApplicationExtension() = default;    ~ExtendPlatformApplicationExtension() noexcept override = default;    voidAddedToApplication(WsfApplication& aApplication)override    {        // 将脚本接口类添加到脚本类型处理逻辑中        aApplication.GetScriptTypes()->Register(ut::make_unique<ScriptExtendPlatformClass>            ("ExtendPlatform"GetApplication().GetScriptTypes()));    }    voidScenarioCreated(WsfScenario& aScenario)override    {        // 将扩展的platform类型添加到平台类型列表        WsfPlatformTypes::Get(aScenario).Add("EXTEND_PLATFORM",            ut::make_unique<ExtendPlatform>(aScenario));    }};extern "C"{   WSF_PLUGIN_EXPORT voidWsfPluginVersion(UtPluginVersion& aVersion)   {       aVersion = UtPluginVersion(           WSF_PLUGIN_API_MAJOR_VERSION,           WSF_PLUGIN_API_MINOR_VERSION,           WSF_PLUGIN_API_COMPILER_STRING           );   }   WSF_PLUGIN_EXPORT voidWsfPluginSetup(WsfApplication& aApplication)   {       // 注册本插件工程       // 此处使用自定义的Application扩展,ExtendPlatformApplicationExtension       aApplication.RegisterExtension("register_PlatformExtendPlugin",           ut::make_unique<ExtendPlatformApplicationExtension>());   }}

脚本接口类ScriptExtendPlatformClass封装的方法可以在txt文件中使用,就是将程序中的方法公布到txt文件中,实现往下看。

3)脚本接口类

脚本接口类提供了向txt脚本编写时能够访问dll中封装的接口,AFSim具有相当丰富的脚本接口,该接口定义了用户可以从WsfPlatform等定义的实体中调用的脚本方法。开发人员可以通过声明和定义可调用的新脚本方法,并将它们添加到管理脚本的UtScriptClass来扩展此接口:

    UT_DECLARE_SCRIPT_METHODUT_DEFINE_SCRIPT_METHODAddMethodAddStaticMethod

    上面这几种脚本定义宏,大家可查看sensor这篇ppt内容的介绍来了解使用方法。

    本文以获取和设置平台造价(Cost)为例,实现了脚本接口类的实现代码(注:必须要重写Clone,原因见上面流程说明):

    // ExtendPlatform.h#pragma once#include"WsfPlatform.hpp"class ExtendPlatform : public WsfPlatform{public:    ExtendPlatform(const WsfScenario& aScenario);    ~ExtendPlatform() override;    doubleGetCost()constreturn m_Cost; }    voidSetCost(double cost){ m_Cost = cost; }    // 返回脚本接口类的名称,即ExtendPlatform    constcharGetScriptClassName()constoverridereturn "ExtendPlatform"; }    //! @name Framework methods    //@{    // 注:必须加Clone方法,否则无法触发ProcessInput调用    WsfPlatform* Clone()constoverride;    boolProcessInput(UtInput& aInput)override;    //@}protected:    double m_Cost = 0// 造价,单位:百万};#include"UtScriptClassDefine.hpp"#include"script/WsfScriptPlatformClass.hpp"class ScriptExtendPlatformClass : public WsfScriptPlatformClass{public:    ScriptExtendPlatformClass(        const std::string& aClassName,        UtScriptTypes* aTypePtr);~ScriptExtendPlatformClass() noexcept override = default;    UT_DECLARE_SCRIPT_METHOD(GetCost); // 声明,获取造价,double cost = <x>.GetCost();    UT_DECLARE_SCRIPT_METHOD(SetCost); // 声明,设置造价,<x>.SetCost(double cost);};
    // ExtendPlatform.cpp#include"ExtendPlatform.h"ExtendPlatform::ExtendPlatform(const WsfScenario& aScenario)    : WsfPlatform(aScenario){}ExtendPlatform::~ExtendPlatform(){}WsfPlatform* ExtendPlatform::Clone()const{    return new ExtendPlatform(*this);}boolExtendPlatform::ProcessInput(UtInput& aInput){    bool ret = true;    // TODO    return ret;}#include"script/WsfScriptContext.hpp"ScriptExtendPlatformClass::ScriptExtendPlatformClass(    const std::string& aClassName,     UtScriptTypes* aTypePtr)    : WsfScriptPlatformClass(aClassName, aTypePtr){    SetClassName("ExtendPlatform");    // 添加脚本可访问的方法    AddMethod(ut::make_unique<GetCost>());   // double cost = <x>.GetCost();    AddMethod(ut::make_unique<SetCost>());   // <x>.SetCost(double cost);}// 脚本方法实现// double cost = <x>.GetCost();// 参数:脚本接口类、扩展的平台类、脚本方法、参数数量、返回值类型、参数类型【多个参数格式: "string, int"】UT_DEFINE_SCRIPT_METHOD(ScriptExtendPlatformClass, ExtendPlatform, GetCost, 0"double"""){    // 这里的aObjectPtr就是对应的扩展平台类ExtendPlatform    double cost = aObjectPtr->GetCost();    // 返回给脚本调用方    aReturnVal.SetDouble(cost);}// 返回值为空也要写上voidUT_DEFINE_SCRIPT_METHOD(ScriptExtendPlatformClass, ExtendPlatform, SetCost, 1"void""double"){    // 获取传入的参数值    double cost = (double)aVarArgs[0].GetDouble();    // 设置到对象    aObjectPtr->SetCost(cost);}

    上面的代码已经能编译成功了,并且在wsf_plugins目录下已经生成了插件dll,这时运行的话可能不会有任何反应,最多在PluginRegistration方法中的断点会进,因为我们还没有针对扩展的平台写任何脚本

    4)语法文件

    要想让wizard在编写我们扩展的平台类EXTEND_PLATFORM给出自动补全提示信息的话,需要先定义语法文件.ag,并将语法文件放到bin下的所有grammar目录。详细的语法规则可在文档中搜索Grammar Guide和Grammar Format查看。

    扩展的类型为EXTEND_PLATFORM,并添加了一个自定义属性Cost,因此需要在语法文件中标明此类是继承至Platform,还要指明编写Cost时的提示信息,如关键字、输入值和可选的单位列表:

    # extend_platform.ag# Extended property(rule money-unit {   (nocase { yuan | dollar })})# Extended platform type(struct EXTEND_PLATFORM   :symbol (type platformType EXTEND_PLATFORM)   :base_type Platform   (var Real cost){# Extended property    cost <Real> <money-unit>    | <Platform>})

    5)脚本参数读取

    这个主要通过重写WsfPlatform的ProcessInput来实现,需要注意的是我们为cost设置了单位,因此在ProcessInput要对单位进行处理,代码如下:

    boolExtendPlatform::ProcessInput(UtInput& aInput){    bool ret = true;    std::string command(aInput.GetCommand());    if (command == "cost")    {        double cost;        aInput.ReadValue(cost);        std::string costUnit;        aInput.ReadCommand(costUnit);        if (costUnit == "dollar")        {            // 测试,1dollar = 6yuan            cost = cost * 6;        }        else if (costUnit == "yuan")        {  }        else        {  }        this->SetCost(cost);    }    else    {        ret = WsfPlatform::ProcessInput(aInput);    }    return ret;}

    6)运行问题

    按上面完成代码编写后,编译运行会在下方的函数中触assert(found)断言:

    这里的意思是,在继承类中找不到父类定义的脚本接口函数,因此会报错。详细调试发现在我们扩展的ScriptExtendPlatformClass对象中有一些附加的脚本方法缺失,造成上面的方法在检测时报找不到匹配的接口方法,比如下图的RadarSigState脚本接口,

    原因见下图,因为在Add时,只会添加到父类WsfPlatform中,在继承类中不会添加。

    因此,这里需要修改源代码,将上图的方法改为可以传入继承类的名称,来将接口方法添加到继承类中。当然文末还有一种不修改源码的方式。

    5

     测试

    上面编译好插件,就可以打开wizard来编写脚本了,我这里通过定义一个从扩展的EXTEND_PLATFORM类型创建一个platform,并编写两个定时script,一个用于获取当前造价,并输出到控制台;一个用于每次script触发都将当前造价值增加1,并更新造价。这里面调用的方法,就是在脚本接口类ScriptExtendPlatformClass声明并定义的接口方法。

    platform F-22 EXTEND_PLATFORM   icon f-22   side blue   cost 12 yuan   add processor addCost WSF_SCRIPT_PROCESSOR      update_interval 2 s      on_update         var cost = ((ExtendPlatform)PLATFORM).GetCost();         cost += 1;         ((ExtendPlatform)PLATFORM).SetCost(cost);      end_on_update   end_processor   add processor showCost WSF_SCRIPT_PROCESSOR      update_interval 1 s      on_update         var cost = ((ExtendPlatform)PLATFORM).GetCost();         writeln("Current Cost: ", cost);      end_on_update   end_processorend_platformend_time 2 h

    从wizard中启动warlork,就可以看到控制台定时输出了递增的造价数据:

    6

     后记

    上面是把扩展platform的流程打通了,是对官方trainning的补漏,没有具体的复杂业务处理。因为需要对源码进行简单修改,才能扩展platform,而平台类在afsim中仅作为承载的容器,其他功能算法都由不同的component来完成的,所以大家根据需要进行选择

    我这里也尝试写成了cmake的插件,只需要放到源码的wsf_plugins目录下并把修改后的几个文件替换afsim源码,然后一起cmake生成编译,它会自动将grammar和dll安装到对应的目录去。

    有需要的请扫码购买:
    另外,有朋友提到可以用下面的方式来扩展platform,从而避免对源码的修改,大家可以尝试一下。
    boolScriptXXPlatform::Initialize(){    // 重写 Initialize 以避免方法索引对齐导致的崩溃    // 跳过基类 Initialize 中的索引对齐逻辑(UtScriptClass.cpp:579-654)    // 但保留其他必要的初始化步骤    bool ok = true;    // 检查方法索引是否正确(来自 UtScriptClass.cpp:673-690)    for (size_t i = 0; i < GetMethodCount(); ++i)    {        InterfaceMethod* methodPtr = GetMethodEntry(i);        if (methodPtr)        {            // 检查索引一致性            if (i != methodPtr->GetIndex())            {                // 索引不匹配,但我们不报错,因为这是预期的                // 父类方法的索引可能与子类列表位置不同            }            // 初始化方法(解析参数类型和返回类型)            if (!methodPtr->Initialize())            {                ok = false;            }        }    }    return ok;}

    通过重写初始化方法避免了修改源码解决了,运行没有问题,可以调用wsfplatfom的方法和新建类的方法。

    DIS分布式仿真

    创建AFSim开发环境

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

    aer回放文件结构解析