IEC 61850标准协议解读 2.基于Java的MMS实现
专栏文章目录
第一章 IEC 61850标准协议解读 0.导言
第二章 IEC 61850标准协议解读 1.建模讲解
第三章 IEC 61850标准协议解读 2.基于Java的MMS实现
目录
- 专栏文章目录
- 前言
- 1 依赖库引入
- 2 创建服务端
- 3 创建客户端
- 4 读写模型
- 4.1 服务端读写
- 4.2 客户端读写
- 5.报告
- 6 文件服务
- 6.1 读取文件目录
- 6.2 读取文件内容
- 6.3 删除文件
- 6.4 上传(写)文件(建设中)
- 7 Goose服务(建设中)
- 附录&参考资料
前言
1 依赖库引入
这个依赖库起先于beanit/iec61850bean,博主在使用过程中,有一些问题(没有文件服务、对接南瑞的1.0客户端测试软件有问题、模型中枚举类型索引和code不能同时支持等),所以修复这些问题并发布到了maven的中央仓库。有兴趣的小伙伴欢迎参与建设,提交pr和issues
<!-- https://github.com/mujave/iec61850bean -->
<dependency><groupId>com.github.mujave</groupId><artifactId>iec61850bean</artifactId><version>1.9.1.11</version>
</dependency>
2 创建服务端
public class SimpleServerClientTest{private static final Logger log = LoggerFactory.getLogger(SimpleServerClientTest.class);private static final int PORT = 102;private static final String ICD_FILE = "src/test/resources/simple-test.icd";//服务端能力对象private ServerSap serverSap;//模型文件能力对象private ServerModel serverModel;private void startServer() throws SclParseException, IOException {//从本地加载一个icd文件,并在102端口暴漏一个mms服务端serverSap = new ServerSap(PORT, 0, null, SclParser.parse(ICD_FILE).get(0), null);this.serverModel = this.serverSap.getModelCopy();}
}
3 创建客户端
public class SimpleServerClientTest implements ClientEventListener{private static final Logger log = LoggerFactory.getLogger(SimpleServerClientTest.class);ClientAssociation clientAssociation;private ServerModel clientModel;private void startClient() throws IOException, ServiceError {//创建客户端能力对象ClientSap clientSap = new ClientSap();//与服务端建立连接this.clientAssociation =clientSap.associate(InetAddress.getByName("localhost"), PORT, "", this);//获取模型文件能力对象this.clientModel = this.clientAssociation.retrieveModel();// 客户端也可以离线的方式读取本地的模型文件//this.clientModel = SclParser.parse(ICD_FILE).get(0);}@Overridepublic void newReport(Report report) {// 这一部分在5章节详细说明}@Overridepublic void associationClosed(IOException e) {log.error("Iec61850 mms server has closed");}
}
4 读写模型
4.1 服务端读写
public void testSetValueForServer() throws IOException, ServiceError, InterruptedException {List<BasicDataAttribute> writeList = CollUtil.newArrayList();//通过模型中DA的引用名称找到对应的对象BdaBoolean v1 = (BdaBoolean) serverModel.findModelNode("FKMONT/GGIO1.Ind1.stVal", Fc.ST);//设置对应要写入的值v1.setValue(false);//将DA加入到待写入集合中writeList.add(v1);BdaFloat32 v2 = (BdaFloat32) serverModel.findModelNode("FKMONT/GGIO2.AnInd1.mag.f", Fc.MX);//读取模型该节点的当前值System.out.println(v2.getFloat().floatValue());v2.setFloat(1.2f);writeList.add(v2);//服务端通过服务端能力对象将数据集写入到模型serverSap.setValues(writeList);
}
4.2 客户端读写
public void testSetValueForClient() throws IOException, ServiceError, InterruptedException {BdaBoolean v1 = (BdaBoolean) clientModel.findModelNode("FKMONT/GGIO1.Ind1.stVal", Fc.ST);System.out.println(v1.getValue()); //falseBdaFloat32 v2 = (BdaFloat32) clientModel.findModelNode("FKMONT/GGIO2.AnInd1.mag.f", Fc.MX);System.out.println(v2.getFloat());//0.0// 调用4.1的方法模拟服务端数据变化testSetValueForServer();//通过客户端能力对象读取服务端模型中的最新数据clientAssociation.getDataValues(v1); System.out.println(v1.getValue());//trueclientAssociation.getDataValues(v2);System.out.println(v2.getFloat().floatValue()); //1.2
}
5.报告
在上一篇博客在3.3.3部分里介绍到关于报告的模型定义,这里我在复制一下
<LN0 inst="" lnClass="LLN0" lnType="GEXIN_LLN0"><!-- 数据集:顾名思义就是数对对象的集合,定义数据集之后,使用 ReportControl报告定义这些数据发生变动时发送报告FCDA 各属性ldInst:逻辑设备实例名称lnClass:逻辑节点类型 lnInst:逻辑节点实例号 对应DO的name.目前国内规范一般按照遥信、遥测分为两各数据集,一个报告遥测量浮点型,一个报告遥信量布尔型--><DataSet name="ds01Din" desc="遥信单点信息数据集(含可控点)"><FCDA doName="Ind1" fc="ST" ldInst="MONT" lnClass="GGIO" lnInst="1"/><FCDA doName="Ind1" fc="ST" ldInst="MONT" lnClass="GGIO" lnInst="3"/></DataSet><!-- 引用ds01Din数据集--><ReportControl bufTime="0" buffered="false" confRev="1" datSet="ds01Din" intgPd="30000" name="brcb01Din" rptID="MONT/LLN0$BR$brcb01Din"><!-- 其中数据对象的dchg、qchg当数据变化、品质变化时都触发报告进行上送 --><TrgOps dchg="true" dupd="true" period="false" qchg="true"/><OptFields dataSet="true" entryID="true" reasonCode="true" seqNum="true" timeStamp="true"/><!-- max属性是IED可以支持的报告实例个数。IED初始化时为每个报告生成max个实例,分别以报告控制块名+实例号(01,02...)进行区分,如brcb01DinO1、brcb01Din02。每个client在连接时以不同的报告实例号占用一个报告实例。每个报告实例按照client指定的属性上送报告--><RptEnabled max="5"/></ReportControl>
</LN0>
@Test
public void reportEnableTest() throws ServiceError, IOException {HashSet<Object> enableReportNamees = new HashSet<>();Collection<Urcb> urcbs = this.clientModel.getUrcbs();for (Urcb urcb : urcbs) {clientAssociation.getRcbValues(urcb);String rptId = urcb.getRptId().getStringValue();log.info("1.{}(rptID:{}) {}", urcb.getName(), rptId, urcb.getRptEna().getValue());if (!enableReportNamees.contains(rptId)) {// 同一个rptId开启一次报告使能就可以clientAssociation.enableReporting(urcb);enableReportNamees.add(rptId);}}for (Urcb urcb : urcbs) {clientAssociation.getRcbValues(urcb);log.info("2.{}(rptID:{}) {}", urcb.getName(), urcb.getRptId().getStringValue(), urcb.getRptEna().getValue());}}
输出打印如下:
1.brcb01Din05(rptID:MONT/LLN0$BR$brcb01Din) false
1.brcb01Din02(rptID:MONT/LLN0$BR$brcb01Din) false
1.brcb01Din01(rptID:MONT/LLN0$BR$brcb01Din) false
1.brcb01Din04(rptID:MONT/LLN0$BR$brcb01Din) false
1.brcb01Din03(rptID:MONT/LLN0$BR$brcb01Din) false2.brcb01Din05(rptID:MONT/LLN0$BR$brcb01Din) true
2.brcb01Din02(rptID:MONT/LLN0$BR$brcb01Din) false
2.brcb01Din01(rptID:MONT/LLN0$BR$brcb01Din) false
2.brcb01Din04(rptID:MONT/LLN0$BR$brcb01Din) false
2.brcb01Din03(rptID:MONT/LLN0$BR$brcb01Din) false
开启报告使能之后,对应数据集的数据在服务端变更时(或者开启周期发送后)客户端就会收到报告了,这里你的客户端需要实现一下ClientEventListener接口,在newReport方法中就会有回调数据过来了,这里就不再赘述,打印进行解析就可以了
@Test
public void reportTest() throws ServiceError, IOException, InterruptedException {//让客户端开启报告reportEnableTest();//调用服务端发送数据testSetValueForServer();
}@Override
public void newReport(Report report) {System.out.println("got a report.");System.out.println(report);
}
输出打印如下:
got a report.
Report ID: MONT/LLN0$BR$brcb01Din
Data set reference: FKMONT/LLN0.ds01Din
Sequence number: 0
Time of entry (unix timestamp): 1745846525390
Reported data set members:
FKMONT/GGIO1.Ind1 [ST]
FKMONT/GGIO1.Ind1.stVal: false
FKMONT/GGIO1.Ind1.q: 0000
FKMONT/GGIO1.Ind1.t: 1970-01-01T00:00:00Z, reason: data-change
6 文件服务
文件服务在原本的beanit/iec61850bean中,仅支持客户端的实现,如果你是服务端的话,需要换成我在文章开头的pom,因为我修改了源码,使其服务端支持了文件(目录)读取的能力。
服务端实现的时候,需要在服务端启动之后设置文件服务的根路径,具体以代码如下//客户端读取到的所有文件内容都是基于这个路径下的,如果不设置的话,就是程序的启动目录 serverSap.setFileServiceParentPath("/home/test"); //客户端读取目录时,是否上报子文件夹,如果设置false,则仅上报根目录下的文件 serverSap.setReportFileDirectory(false);
6.1 读取文件目录
@Test
public void testGetFileDirectory() throws IOException, ServiceError, InterruptedException {List<FileInformation> fileDirectory = this.clientAssociation.getFileDirectory("COMTRADE");int i = 0;for (FileInformation fileInformation : fileDirectory) {log.info("{} - {} sizeof: {} {}", ++i, fileInformation.getFilename(), fileInformation.getFileSize(),DateUtil.formatDateTime(fileInformation.getLastModified().getTime()));}
}
6.2 读取文件内容
@Test
public void testGetFile() throws IOException, ServiceError, InterruptedException {this.clientAssociation.getFile("/chart.txt", (byte[] fileData, boolean moreFollows) -> {log.info("Received {} bytes of file data. More data follows: {}", fileData.length, moreFollows);//这里直接进行保存,可以使用追加方式写文件log.info("\n{}", new String(fileData));return moreFollows;});
}
6.3 删除文件
@Test
public void testDeleteFile() throws ServiceError, IOException {this.clientAssociation.deleteFile("要删除的文件名字");
}
6.4 上传(写)文件(建设中)
客户端写文件到服务端正在开发中,预计在2025.05月底完成,持续更新
7 Goose服务(建设中)
客户端写文件到服务端正在开发中,预计在2025.07月底完成,持续更新
附录&参考资料
暂无,更新中