SDK和半自动代码生成工具

通过上面的学习,我们发现驱动代码具备高度相似的结构,为了提高研发效率,我们提供了代码生成工具,集成在SDK中,大家平时可以使用代码生成工具先生成基础代码,再进行修改。

SDK下载地址:

http://github.com/fsuos/fsuossdk2

http://gitee.com/fsuos/fsuossdk2

当前SDK版本是2,使用此SDK版本开发的驱动与当前正在使用FsuOS 2版本兼容。

SDK生成代码时会同时生成c++和python的代码,在3030PRO-NG等更新版本的FSU上我们建议优先使用python驱动,在不支持Python的FsuOS版本上再使用C++驱动,python驱动的维护成本具备明显优势。但是C++程序在低配置的FSU上拥有明显的性能优势,仍具备广泛的适配性。

注意: 请不要自行修改SDK中提供的头文件,由于头文件设计各种基础类的内存大小,在libvdevice.so编译中已经确定,为了保证原有驱动和新写驱动都能在FsuOS 2的系统中加载,我们已经不修改SDK的头文件,除非遇到特别致命的功能缺陷,我们会进行修改并通知进行大规模的更新,修改头文件后,必须将原有的SMDDevice,libvdevice和所有使用的驱动都重新编译,才能保证新系统顺利运行。如果维护几万套系统,升级是个痛苦的过程,但是不升级新驱动又没法使用。

代码生成器是使用 https://github.com/kolypto/j2cli 工具,大家克隆SDK后,需要自己安装j2工具,相关文档可以自己查看 https://jinja.palletsprojects.com/en/3.1.x/

pip install j2cli[yaml]

根据自己使用的Linux发行版本的不同,命令可以会稍有区别,比如Opensuse下,需要使用

pipx install j2cli[yaml]

不需要root用户,普通用户即可。

FusOS SDK的目录结构如下:

├── cmake-fsu-drivers.sh      自动生成驱动包的脚本
├── Config                    配置驱动生成器的配置文件目录
│   └── DTSD3366D.yml
├── Drivers                   自动生成的驱动工程文件和CMakeLists.txt,工程文件用codelite可以打开,对代码进行调整,CMakeLists.txt可以被cmake-fsu-drivers.sh直接使用
│   └── SPDTSD3366D           示例驱动
├── gcc                       针对不同型号FSU提供的编译器配置文件
│   ├── arm-303MINI-toolchain.cmake
│   ├── arm-303X-toolchain.cmake   303X FSU使用的编译器配置文件
│   ├── arm-GFSU-toolchain.cmake
│   ├── arm-jgcx41-linux      303X FSU使用的编译器
│   ├── arm-ZNV-EISU-toolchain.cmake
│   ├── arm-ZNV-IG2000-toolchain.cmake
│   └── arm-ZNV-IG2100-toolchain.cmake
├── gen_di_driver.sh      自动生成DI型驱动代码的脚本
├── gen_ai_driver.sh      自动生成AI型驱动代码的脚本
├── gen_modbus_driver.sh      自动生成SP型Modbus驱动代码的脚本
├── gen_pmbus_driver.sh      自动生成SP型pmbus电总驱动代码的脚本
├── gen_pmbus419_driver.sh      自动生成SP型pmbus电总开关电源代码的脚本
├── gen_sp_driver.sh      自动生成SP型驱动代码的脚本
├── include                   SDK的头文件
├── interface                 SDK的接口文件
├── Projects                  自动生成的驱动源代码
└── template                  SDK代码模板

FsuOS SDK会自动生成DI,AI,串口类(Modbus,电总类,自定义协议)的驱动代码和界面代码,对于简单的协议,基本可以直接使用,其他的通过少量调整即可使用,能快速解决80%以上的驱动编写问题。
使用方法:
例如我们现在要编写DTSD3366D,首先在Config目录下,编写DTSD3366D.yml配置文件,这个文件名和类名需要保持一致,一般采用全大写或者首字母大写。

我们称DTSD3366D.yml配置文件为DriverFile,即配置驱动然后自动生成可执行驱动的文件,意思类似DockerFile,即可以从这个文件自动生成出完整的Docker容器。 DriverFile会逐步代替一些简单驱动的代码和工程文件,届时,驱动的源文件将以DriverFile的形式表现,用户不再需要操作复杂的C++代码和PHP解析代码,仅仅通过配置调整DriverFile即可完成设备驱动的编写。

由于FsuOS的代码还在快速发展中,代码生成器支持的语法也在快速变化,除了本文档列出的部分内容,建议大家可以参考Config文件夹下的DriverFile自行学习和理解,并根据自己需要修改template里的模板代码。

Project:
  Name : DTSD3366D
  RT_ID : 5156
  File : "测试.pdf"
InitSetting :
  - Name : ct
    Type : int
  - Name : has_a
    Type : int
Sample :
  - Cmd : 3
    Offset : 7
    Len : 4
    Data :
      - Name : A相电压
      - Name : B相电压
        Offset : 3
  - Cmd : 3
    Offset : 0x16E
    Len : 44
Threshold :
  - Bool : True
    Level : 1
    SignalId : "0777001"
    SignalName : "停单告警"
    SignalDesc : "停单告警"
    Value : (cData.r3_7[0]>>2)&0x1
    SignalIndex : 1
  - Bool : False
    Key : voltage
    Name : 电压
    Value : ((float)cData.r3_7[0])/100
Value :
  - Name : Ua
    Value : cData.r3_7[0]
  - Name : Ub
    Value : cData.r3_7[1]
AO :
  - SignalId : 1234567
    Desc : 测试1
  - SignalId : 1234568
    Desc : 测试2
DO :
  - SignalId : 1234567
    Desc : 测试3
  - SignalId : 123456

这个配置文件详解最下面讲:

生成驱动代码的步骤:

  1. 根据生成驱动类型的不同,需要执行不同的脚本生成脚本
驱动类型 子类型 脚本名称
DI型 - ./gen_di_driver.sh
AI型 - ./gen_ai_driver.sh
SP型 Modbus ./gen_modbus_driver.sh
SP型 Pmbus ./gen_pmbus_driver.sh
SP型 Pmbus419 ./gen_pmbus419_driver.sh
SP型 通用型 ./gen_sp_driver.sh

本例: ./gen_modbus_driver.sh DTSD3366D

执行成功,则会在对应的目录下生成对应的文件,DI会在SMDDIProcessor,AI会在SMDAIProcessor,SP会在SMDSPProcessor,Socket会在SMDSocketProcessor, 同时DeviceWeb下会生成php代码的helper和view文件。Drivers下会生成codelite的工程文件和CMakeLists.txt

SMDSPProcessor/
├── DTSD3366D.cpp
├── DTSD3366D.h
└── DTSD3366D_interface.h

DTSD3366D_interface.h

#ifndef DTSD3366D_INTERFACE_H
#define DTSD3366D_INTERFACE_H

#include "common_interface.h"

#pragma pack(push)
#pragma pack(1)


struct DTSD3366D_Data_t {
    unsigned int data_id;
    uint16_t r3_7[4];
    uint16_t r3_366[44];
    tele_c_time update_time;
};

#pragma pack(pop)
#endif

DTSD3366D.h

#ifndef DTSD3366D_H
#define DTSD3366D_H
#include "SPModbus.h"
#include "DTSD3366D_interface.h"
#include "UniDataDevice.h"


/**
 * @file 测试.pdf
 * @brief
 */
#define RT_DTSD3366D 5156
class DTSD3366D: public UniDataDevice<DTSD3366D_Data_t, SPModbus, RT_DTSD3366D>
{
public:
    DTSD3366D();
    ~DTSD3366D();
    bool process_payload(enum tab_type type, size_t len) override;
    bool RefreshStatus() override;
    float Get_Value(uint32_t data_id, const std::string& var_name) const;
private:
    enum DTSD3366D_Status {
        DTSD3366D_IDLE = 10,
        DTSD3366D_R3_7,
        DTSD3366D_R3_366,
        DTSD3366D_END
    };
};

PLUMA_INHERIT_PROVIDER(DTSD3366D, SMDSPDevice);
#endif

DTSD3366D.cpp

#include "DTSD3366D.h"
#include "UniDataDevice.cpp"


DTSD3366D::DTSD3366D()
{
    device_type_ = "dtsd3366d";
    baud_rate_ = 9600;
    addr_ = 1;
    //save_interval_ = 600;
}

DTSD3366D::~DTSD3366D()
{
}


bool DTSD3366D::RefreshStatus()
{
    SMDSPDevice::RefreshStatus();
    state = DTSD3366D_R3_7;
    modbus_read_registers(7, 4);
    return true;
}

bool DTSD3366D::process_payload(enum tab_type type, size_t len)
{
    switch(state){

      case DTSD3366D_R3_7:{
            memcpy(cData.r3_7, tab_reg, sizeof(uint16_t)*4);
            state = DTSD3366D_R3_366;
            modbus_read_registers(366, 44);
            break;
      }
      case DTSD3366D_R3_366:{
            memcpy(cData.r3_366, tab_reg, sizeof(uint16_t)*44);
            RoundDone();
            return false;
      }
   }
   return true;
}

float DTSD3366D::Get_Value(uint32_t data_id, const std::string& var_name) const
{
    if(!bIsDataReady_)
        throw std::out_of_range("数据未就绪");
    boost::posix_time::ptime now = boost::posix_time::second_clock::local_time();
    boost::posix_time::time_duration diff = now - lastTime;
    if( diff.total_seconds() > 60) {
        throw std::out_of_range("数据已超时");
    }

    /*if(var_name == "U") {
        return cData.data[0];
    }*/
    throw std::out_of_range("不支持变量");
}


#ifdef USE_SEPERATE_DRIVER

extern "C"
std::vector<std::shared_ptr<Provider>> get_providers()
{
    std::vector<std::shared_ptr<Provider>> providerVec;
    providerVec.push_back(std::make_shared<DTSD3366DProvider>());
    return std::move(providerVec);
}

#endif

Drivers目录下自动生成工程文件,具体内容自行查看:

Drivers/
└── SPDTSD3366D
    ├── CMakeLists.txt
    └── SPDTSD3366D.project

这时候,可以使用codelite打开SPDTSD3366D.project文件,即可正常编辑

调用 ./cmake-fsu-drivers.sh SPDTSD3366D 即可对驱动进行编译和打包
注意: 由于这个脚本不区分驱动的类型,因此需要用户传递DI,AI,SP前缀到驱动名前

marship@linux-afl3:~/PythonProjects/FsuOSSDK2> ./cmake-fsu-drivers.sh SPDTSD3366D
CMake Deprecation Warning at CMakeLists.txt:3 (cmake_minimum_required):
  Compatibility with CMake < 3.5 will be removed from a future version of
  CMake.

  Update the VERSION argument <min> value or use a ...<max> suffix to tell
  CMake that the project does not need compatibility with older versions.


-- The C compiler identification is GNU 10.2.0
-- The CXX compiler identification is GNU 10.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /home/marship/PythonProjects/FsuOSSDK2/gcc/arm-jgcx41-linux/bin/arm-jgcx41-linux-gnueabihf-gcc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /home/marship/PythonProjects/FsuOSSDK2/gcc/arm-jgcx41-linux/bin/arm-jgcx41-linux-gnueabihf-g++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.6s)
-- Generating done (0.0s)
CMake Warning:
  Manually-specified variables were not used by the project:

    CMAKE_TOOLCHAIN_FILE


-- Build files have been written to: /home/marship/PythonProjects/FsuOSSDK2/cmake-build-Arm-A7-Debug/SPDTSD3366D
[1/2] Building CXX object CMakeFiles/SPDTSD3366D.dir/home/marship/PythonProjects/FsuOSSDK2/Projects/DTSD3366D.cpp.o
/home/marship/PythonProjects/FsuOSSDK2/Projects/DTSD3366D.cpp: In function ‘std::vector<std::shared_ptr<Provider> > get_providers()’:
/home/marship/PythonProjects/FsuOSSDK2/Projects/DTSD3366D.cpp:69:21: warning: moving a local object in a return statement prevents copy elision [-Wpessimizing-move]
   69 |     return std::move(providerVec);
      |            ~~~~~~~~~^~~~~~~~~~~~~
/home/marship/PythonProjects/FsuOSSDK2/Projects/DTSD3366D.cpp:69:21: note: remove ‘std::move’ call
[2/2] Linking CXX shared library /home/marship/PythonProjects/FsuOSSDK2/cmake-build-Arm-A7-Debug/output/dtsd3366d.so

配置文件YAML格式说明

整体分为7个小节,对应代码的7个部分:

- Project:               工程参数
  Name : DTSD3366D       类名
  RT_ID : 5156           RT_类名的ID,可以认为是类别编码,或者类编码
  File : "测试.pdf"       对应的协议文档

- InitSetting :   对应是InitSetting函数,自动解析逻辑参数
  - Name : ct              逻辑参数名
    Type : int             逻辑参数类型
  - Name : has_a
    Type : int
- Sample :   对应的是采集功能
  - Cmd : 3          ModBus的命令,可选1,2,3,4,其中1,2,是read coils, 3,4是read registers
    Offset : 7       读取的地址
    Len : 4          读取的长度,本配置会在cData中生成r3_7的数组,长度为4
    Data :           解析代码,可以做简单的解析,在php的helper中使用
      - Name : A相电压   解析的数据的中文名字,Offset从1开始,如果没有,则按数组顺序。如果有,则使用设置的次序
      - Name : B相电压      
        Offset : 3   
  - Cmd : 3
    Offset : 0x16E     地址是原样生成到代码里,所以可以使用各种C合适的写法。
    Len : 44
Threshold :          告警规则的代码
  - Bool : True      若为True,则生成CheckThresholdBool,若未false,则生成CheckThreshold  
    Level : 1              级别  
    SignalId : "0777001"   信号ID   
    SignalName : "停单告警"  信号名称  
    SignalDesc : "停单告警"  信号描述  
    Value : (cData.r3_7[0]>>2)&0x1   告警判断值  
    SignalIndex : 1         信号通道号,可忽略,自动生成  
  - Bool : False        
    Key : voltage         告警的key值  
    Name : 电压            告警的名称  
    Value : ((float)cData.r3_7[0])/100  告警判断值  
RunCheckThresholdCode: |   这个是原始的C++驱动代码,用户可以在这部分填写定制部分,会原样输出到RunCheckThreshold函数尾部
    if ( 1 == 1) {
        }
Value :                   用于动态配置关联的代码,Get_Values    
  - Name : Ua                  用户可以绑定的变量名  
    Value : cData.r3_7[0]       实际值  
  - Name : Ub  
    Value : cData.r3_7[1]  
AO :                         联通/电信B接口的AO控制代码
  - SignalId : 1234567        AO信号ID
    Desc : 测试1              AO信号描述,用途等
  - SignalId : 1234568
    Desc : 测试2
DO :                        联通/电信B接口的DO控制代码
  - SignalId : 1234567       DO信号ID
    Desc : 测试3              DO信号描述,用途等
  - SignalId : 123456

ArrayName和ArrayLength, 用于解决比如有120个电池电压,同类型且连续。

  - Cmd : 3
    Offset : 0x012A
    Len : 18
    Data :   
      - ArrayName : "%d#外扩温度"
        ArrayLength : 18
        Unit : "℃"
        Ratio : 10
        Offset : 1

对18个寄存器,按数组进行解析。

BlockTemplate 模板支持

在协议解析的过程中,我们经常会遇到重复性的数据,对于复杂的结构,我们需要一种可重复使用的解析代码,BlockTemplate就是这个用途。
BlockTemplate: 下面紧跟的,我们可以理解为函数名称,这些函数名称都可以被直接调用的。 参考:UnicomXJSLPLC.yaml 例子1:

  ACTemperature :
    BlockLength : 8
    BlockType : 'f'
    BlockRType : 1
    BlockContent :
      - Name : '%d#机组进水温度实际值'
      - Name : '%d#机组回水温度实际值'

调用这个函数时,需要8个字节的数据,BlockType用float进行解析,BlockRType是处理字节反转的,当BlockRType等于1时,字节0123会被自动调整为2301,相当于前后2个字节对调。BlockContent和Data的结构一样。 生成的代码:

function _unicomxjslplc_ACTemperature(&$dataArray, $memData, $prefix, $index  )
{
    $offset = 0;
        $lMemData = '';
        for($i=0;$i<strlen($memData);$i+=4){
          $lMemData .= $memData[$i+2].$memData[$i+3].$memData[$i].$memData[$i+1];
        }
        $v = unpack("f*" , $lMemData);
      
        $name = $prefix.sprintf("%d#机组进水温度实际值", $index);
          $dataArray[$name] = number_format($v[1], 2);
      
        $name = $prefix.sprintf("%d#机组回水温度实际值", $index);
          $dataArray[$name] = number_format($v[2], 2);
}

例子2:

  ACWaterGroup : 
    BlockLength : 60
    BlockContent : 
      - ArrayBlock : AC
        ArrayStart : 1
        ArrayEnd : 4
      - ArrayBlock : ACStatus
        ArrayStart : 1
        ArrayEnd : 4
        Transform : "bits"
        Length : 8
      - ArrayBlock : ACTemperature
        ArrayStart : 1
        ArrayEnd : 4

调用这个函数时,需要60个字节的数据,内容为:4个AC函数 + 4个ACStatus, 4个ACTemperature, 这个函数展开时,会自动调用对应的函数。 使用:

  - Cmd : 3
    Offset : 0
    Len : 30
    Data : 
      - Block : ACWaterGroup
        index : 0
  - Cmd : 3
    Offset : 30
    Len : 30
    Data : 
      - Block : ACWaterGroup
        index : 4

第一个小节,我们从0读取30个寄存器,就是60个字节,有别于平时Data节直接做解析,我们采用Block : ACWaterGroup, 调用前面的ACWaterGroup进行数据解析,ACWaterGroup也会调用相关的函数。index会传入

我们推荐在生成的代码的基础上,对代码进行调整,以满足需求。然后再将定制代码同步更新到DriverFile中,使用SDK验证DriverFile工作良好,保存好此驱动的DriverFile,后续直接使用DriverFile即可。

问题和考虑:

  1. 告警规则的配置,使用下面这种,太罗嗦。 用了几次,都觉得麻烦,放弃!
Threshold :          告警规则的代码
  - Bool : True      若为True,则生成CheckThresholdBool,若未false,则生成CheckThreshold  
    Level : 1              级别  
    SignalId : "0777001"   信号ID   
    SignalName : "停单告警"  信号名称  
    SignalDesc : "停单告警"  信号描述  
    Value : (cData.r3_7[0]>>2)&0x1   告警判断值  
    SignalIndex : 1         信号通道号,可忽略,自动生成  
  1. 使用 RunCheckThresholdCode 目前反馈较好,因为和以前使用方法一样,大家就是复制粘贴,改一改,做起来还快。
    遇到的第一个问题,如何兼容python驱动,毕竟大部分代码都生成了,没必要为python再单独写一遍。 遇到的第二个问题,如何兼容电信,联通或者更多的不同B接口平台,告警信号ID不同的问题。
    我们考虑过一个方案,内部使用有规律的信号ID,比如r3_1357_1_3,代表寄存器3_1357地址,1索引,位3,然后加载ini的时候,读取里面的映射,虽然还没做,但是已经感觉很复杂了。ID映射的事情,本来在配这个文件的时候,查一遍就完事了,现在还要分成2个环节,出错了,还得2边比对。结论还是直接在yml里写,毕竟复制粘贴更容易一些。
    结论就是:对于上面这个问题,分成不同的配置key: RunCheckThresholdCodeTelecom 电信C++程序用 RunCheckThresholdCodeTelecomPy 电信Python程序用 RunCheckThresholdCodeUnicom 联通C++程序用 RunCheckThresholdCodeUnicomPy 联通Python程序用

  2. 一些比较特殊的配置选项 Sample.Data.Value 自定义计算值

Sample : 
  - Cmd : 3
    Offset : 0
    Len : 33
    Type : "s"
    Data : 
      - Name : "回风温度测量值"
        Unit : "℃"
        Offset : 1
        Ratio : 10
      - Name : "回风温度测量值2"
        Value : ($v[1]/10)

比如上段,是比较标准的配置方式,php代码会自动组合规则,将$v[1]/10加上℃,放到"回风温度测量值"。 有时候,我们这个值比较复杂,可能需要比如乘个PT变比,逻辑参数,此时就可以使用Value : $v[1]+$v[2] , 这是就会使用这个计算值,放到"回风温度测量值"。

Sample.Data.Options 状态的枚举值

      - Name : "电池状态"
        Offset : 3
        Options :
          - Key : 2
            Value : 休眠
          - Key : 3
            Value : 浮充
          - Key : 4
            Value : 均充
          - Key : 5
            Value : 放电

这种翻译出来,就是 值是2,就是 休眠

        switch($v[3]){
          case 2:
          $dataArray["电池状态"] = "休眠";
            break;
          case 3:
          $dataArray["电池状态"] = "浮充";
            break;
          case 4:
          $dataArray["电池状态"] = "均充";
            break;
          case 5:
          $dataArray["电池状态"] = "放电";
            break;
          default:
            $dataArray["电池状态"] = "无效值";
            break;
        }