通过上面的学习,我们发现驱动代码具备高度相似的结构,为了提高研发效率,我们提供了代码生成工具,集成在SDK中,大家平时可以使用代码生成工具先生成基础代码,再进行修改。
SDK下载地址:
http://github.com/fsuos/fsuossdk2
http://gitee.com/fsuos/fsuossdk2
当前SDK版本是2,使用此SDK版本开发的驱动与当前正在使用FsuOS 2版本兼容。
代码生成器是使用 https://github.com/kolypto/j2cli 工具,大家克隆SDK后,需要自己安装j2工具,相关文档可以自己查看 https://jinja.palletsprojects.com/en/3.1.x/。
根据自己使用的Linux发行版本的不同,命令可以会稍有区别,比如Opensuse下,需要使用
不需要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配置文件,这个文件名和类名需要保持一致,一般采用全大写或者首字母大写。
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
这个配置文件详解最下面讲:
生成驱动代码的步骤:
驱动类型 | 子类型 | 脚本名称 |
---|---|---|
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
- 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
- Cmd : 3
Offset : 0x012A
Len : 18
Data :
- ArrayName : "%d#外扩温度"
ArrayLength : 18
Unit : "℃"
Ratio : 10
Offset : 1
对18个寄存器,按数组进行解析。
在协议解析的过程中,我们经常会遇到重复性的数据,对于复杂的结构,我们需要一种可重复使用的解析代码,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即可。
Threshold : 告警规则的代码
- Bool : True 若为True,则生成CheckThresholdBool,若未false,则生成CheckThreshold
Level : 1 级别
SignalId : "0777001" 信号ID
SignalName : "停单告警" 信号名称
SignalDesc : "停单告警" 信号描述
Value : (cData.r3_7[0]>>2)&0x1 告警判断值
SignalIndex : 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程序用
一些比较特殊的配置选项 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;
}