国产CMS61850那些事-总述

近几年随着各类国产化的崛起,无论是硬件还是软件,都开始倡导国产化。电力领域也开始了国产化的进程,包括现在很多项目都明确要求了网关,融合终端等必须是国产芯片,很多省的配电房iot项目也是国产化的应用(关于iot相关的介绍,放到后面了)。

一、背景

我们先看报批稿中关于开展cms61850研究的背景

总的来说呢,61850是一个好的协议,一个好的面向对象的思想,是面向未来的电力协议。但是因为国际版在61850中引入了MMS协议。这个就显得有点多余了,使得协议变得十分复杂,不利于维护更改,性能也比较低。所以在61850引入国内之处,熟悉相应协议的大家都知道,思科的mmslite,迅速引入了项目中,被收割了一波。为了更好的纯粹61850,加入一些安全策略,cms61850就应运而生了。我们再看一张两者对比的架构图

左边为cms61850架构,右边为国际版61850。可以看的出,国内版核心业务,都是直接基于tcp之上,这与国际版在tcp之上各种封装简化了很多,也易于理解。

二、编码

前两年的时候,倒是听过国产61850,但是相应的项目很少,大部分项目用的还是国际版61850。用mmslite足以。但去年的时候,有一些用户开始询问国产61850的事,我就开始准备去研究这个协议。但是公司目前也没有什么开发计划,就业余的时间搞搞了。差不多去年十二月份左右开始研究,后来阳了过后,停了一段时间,今年开始,又继续开整了。

那么问题来了,我们该怎么去开发呢。我觉得开发这个协议至少得又两个必要因素:

一、对61850协议有一定的认识,比如相关概念,SCL语法等都要掌握

二、一定的工作经验加良好的编码能力

当这两点都具备的时候,我们可以尝试着开始了。首先从架构上来看,我门从战略上藐视它,不过也就一个基于TCP的协议。作为开发老鸟,谁还没写过socket相关的程序呢,这个问题不大。剩下的无非就是报文分析呗。就在我们这样想的时候,我们遇到了第一个拦路虎,编码

国际版61850采用的是ber编码,而cms61850采用的是per编码,具体来说是紧缩型per,也就是APER。关于这两个孰优孰劣,其实我觉得没必要去长篇大论分析,按我的猜想,纯粹因为跟国际版区分开选择了per,不然怎么体现国产呢😄。什么能减轻带宽啦,性能提高啦,我觉得不是必要因素

APER编码相关文档,见下面链接

链接:https://pan.baidu.com/s/1oGlkbUw8rHX2WOiKcZ6b6Q

提取码:xm9k

其中的ASN.1编码文档整理写的是相当好,只可惜不知道作者。另一个是规范,比较晦涩难懂,选看。

那我们怎么开发呢?之前的文章,或多或少,我也提到过,站在巨人的肩膀上,会更加顺手。本来我也想自己根据协议文档开发一个,但是后来发现,如果还想实现根据asn文件生成相应代码,那工作量还是不小。这时候,我们需要借鉴三方库了,在asn1编码支持中,比较出名的是asn1c这个代码。立马github克隆下来。可以发现它支持的编码规则还是比较多的,如下:

但是遗憾的是,该库并未实现APER,倒是UPER实现了。但是在评论中又发现了基于该库的另一个分支实现了APER。

果断再次克隆下来,按照流程进行相应APER编解码,然后与上述文档中的一些示例还是不一样。这个APER与国标的还是有一点差别。到这里也不要放弃,毕竟asn1的框架,从编解码,生成相应代码文件这块支持还是比较友好的。所以决定对其进行改造。忘记补充一点,开发CMS61850还得需要一个很重要的本领,调试。调试,可以很方便的帮我们理解陌生代码以及修改相应代码

最终经过修改,得到了能正确编解码的APER。后面会给出相应测试程序。

使用步骤,先编写asn文档,这个直接从审批稿中拷贝就可以,以协商为例内如如下:

然后执行指令 ./asn1c ./61850CMS.asn -D ./out -gen-APER -no-gen-BER -no-gen-OER -no-gen-XER -no-gen-JER -no-gen-example -fcompound-names

就可以生成一堆协商相关的头文件和c文件,是直接可以加入到我们的项目中使用的.

#ifndef    _Associate_RequestPDU_H_
#define    _Associate_RequestPDU_H_


#include <asn_application.h>

/* Including external dependencies */
#include "VisibleString129.h"
#include <OCTET_STRING.h>
#include "UtcTime.h"
#include <constr_SEQUENCE.h>

#ifdef __cplusplus
extern "C" {
#endif

/* Associate-RequestPDU */
typedef struct Associate_RequestPDU {
    VisibleString129_t    *serverAccessPointReference;    /* OPTIONAL */
    struct Associate_RequestPDU__authenticationParameter {
        OCTET_STRING_t     signatureCertificate;
        UtcTime_t     signedTime;
        OCTET_STRING_t     signedValue;
        
        /* Context for parsing across buffer boundaries */
        asn_struct_ctx_t _asn_ctx;
    } *authenticationParameter;
    
    /* Context for parsing across buffer boundaries */
    asn_struct_ctx_t _asn_ctx;
} Associate_RequestPDU_t;

/* Implementation */
extern asn_TYPE_descriptor_t asn_DEF_Associate_RequestPDU;

#ifdef __cplusplus
}
#endif

#endif    /* _Associate_RequestPDU_H_ */
#include <asn_internal.h>

三、实现

其实编码能搞定的话,实现业务来说,无非就是花时间编写调试了。毕竟cms61850实现的接口还是非常多的。这里先简单介绍,相应代码编写,后面代码可能会部分开源,到时会写的更为详细些。仍然以协商为例

/// 注册协商部分处理函数,code为1
CServiceManager::instance()->attachFunc(1, "Associate", base::function(&CAssociate::associate, this));
ServiceError CAssociate::associate(const std::string &funcName, const NetMessage &message, std::string &response)
{
    m_mutex.lock();
    auto info = m_mapTcpInfo[message.clientId];
    m_mutex.unlock();
    info->status = AssociateFail;
    /// 协商一开始状态假定为Fail
    CServiceManager::instance()->setStatus(message.clientId, CServiceManager::AssociateFail);

    CSafeStruct<Associate_RequestPDU> reqPtr;
    /// 解码协商请求,填充到Associate_RequestPDU中
    if (!Decode(reqPtr, message.buf, message.len))
    {
        errorf("deocde %s failed\n", funcName.c_str());
        return ServiceError_decode_error;
    }
    /// 打印请求内容
    PrintAPER(reqPtr);
    auto &sclInfo = CSCLParse::instance()->getSCLInfo();
    if (reqPtr->serverAccessPointReference != NULL && reqPtr->serverAccessPointReference->buf != NULL)
    {
        std::string serverPoint = (char *)reqPtr->serverAccessPointReference->buf;
        if (serverPoint != sclInfo.accessPoint)
        {
            return ServiceError_instance_not_available;
        }
    }
    /// 封装响应结构体
    CSafeStruct<Associate_ResponsePDU> respPtr;
    if (reqPtr->authenticationParameter != NULL)
    {
        /// 应用层安全校验
        if (!verifySoftSafe(reqPtr->authenticationParameter))
        {
            warnf("verify app safe failed\n");
            return ServiceError_failed_due_to_communications_constraint;
        }
        respPtr->authenticationParameter = CallocPtr(Associate_ResponsePDU::Associate_ResponsePDU__authenticationParameter);
        /// 回传检验信息
        if (!setSoftSafeAuth(respPtr->authenticationParameter))
        {
            warnf("set app safe param failed\n");
            return ServiceError_failed_due_to_communications_constraint;
        }
    }
    copyStrToOctStr(m_stationId, &respPtr->associationId);
    respPtr->serviceError = ServiceError_no_error;

    /// 对响应结构体进行编码
    if (!Encode(respPtr, response))
    {
        errorf("encode %s response failed\n", funcName.c_str());
        return ServiceError_failed_due_to_communications_constraint;
    }
    PrintAPER(respPtr);
    info->status = Associate;
    CServiceManager::instance()->setStatus(message.clientId, CServiceManager::SUCCESS);
    return ServiceError_no_error;
}

函数设计返回值为ServiceError,因为有些接口是要又否定响应相应的值。后续的接口开发与此处类似,只需要编写相应处理函数,注册进管理即可。对asn1c熟悉的同学可能会觉得,怎么没有看到asn1c相关的接口呢,上述代码都是经过封装的。比如解码相关接口封装如下:

bool decode(const asn_TYPE_descriptor_s *td, void **sptr, const void *buffer, size_t size)
{
    asn_dec_rval_t ret = aper_decode_complete(td, sptr, buffer, size);
    return ret.code == RC_OK ? true : false;
}

程序中为了减少用户对asn1c代码的理解,秉着学习的越少,用的越好的理念,进行了封装,也就是客户只需要了解简单少许接口,即可编程。比如为了减少代码的书写,为了更好的控制内存释放。在c语言中,比如在一个函数中申请了内存,每一个return之前都需要进行相应释放。显得繁琐啰嗦,所以在很多c代码中,我们可以看到goto语句的应用,进行统一处理。但是这里我们选择c++,就有它自己的处理方式,可以模仿智能指针,写一个简单的智能管理类,同时为了不让客户再去理解asn1c中的接口,进行了封装,如下

template<typename T>
class CSafeStruct;

#define SafeDef(type) \
template<>\
class CSafeStruct<type> {\
using realType = DEFType(type);\
public:\
    explicit CSafeStruct() : m_bRelese(true) {m_ptr = CallocPtr(realType); m_stuPtr = StructPtr(type);}\
    CSafeStruct(realType *ptr) : m_bRelese(false) { m_ptr = ptr; m_stuPtr = StructPtr(type);}\
    ~CSafeStruct() {if (m_bRelese) FreeStruct(type, m_ptr);}\
public:\
    realType *get() {return m_ptr;}\
    realType **getRef(){return &m_ptr;}\
    realType *operator->() {return m_ptr;}\
    realType& operator*() {return *m_ptr;}\
    asn_TYPE_descriptor_t *getTypePtr() {return m_stuPtr;}\
private:\
    realType *m_ptr;\
    asn_TYPE_descriptor_t *m_stuPtr;\
    bool m_bRelese;\
};

在使用中,只要用CSafeStruct进行封装,在函数结束时,会在析构中自动释放内存。

四、结尾

后续代码接口部分,会在后面文章中继续跟新。

已上传部分开源代码,见下:

https://github.com/LinuxZQ93/CMS61850

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值