大家好,又见面了,我是你们的朋友全栈君。
1. 前言
最近在研究广东电网的101与104规约,也就是DL/T634.5101-2002和DL/T634.5104-2009。因为要做一个规约解析的软件(基于Android平台的),刚开始接触的也是一头雾水,因为没有接触过这方面的知识,所以就在网上搜索各种技术帖,大神经验什么的。
后来在网上找到了一个软件–IEC8705(报文翻译工具).exe,这个可以解析一些101(平衡式)的实例,效果图贴一下。
但有些还是解析不了,并且在网上也找不到他的源码,所以就很苦恼。也找到一些C++的源码,但是由于技术有限,我看不懂。下面就和大家来看看我写的这个简单的Java语言的规约解析。先贴一下效果图(我就在控制台简单演示一下,没有做可视化界面)。
GitHub地址:https://github.com/mujave/iec 欢迎star哦
2. Java版效果图
3. 规约详细解释
先看一下我总结的关于规约解析的几个图
101的规约总结
104规约总结
具体的104协议解释也可以参考 https://blog.csdn.net/wojiuguowei/article/details/79413142
4. 名词解释
这里所说的名词解释,并不是对一些名词的定义的解释,因为这不是我的专业领域,下面我主要针对报文所代表的名词在代码里怎样解释(解析)说一下。
- 控制域(平衡式)
在报文解析中,我就是按照图里面的取值方法对对应的字节进行取值,然后进行逻辑判断,将结果加到解析结果里面,由于后四位所代表的平衡链路功能码在这里是固定的,所以可以采取枚举或者反射(常量)的方式进行解析 - 地址域 (以104的信息体地址为例)
在规约中地址才用的低前高后(低位在前高位在后)的规则
例如:68 10 08 00 02 00 09 01 03 00 01 00 01 40 00 2B 02 00(黄色部分为信息体地址)
所以 这个信息体的地址为01 40 00 >> 0x004001=16385 - 时标CP56Time2a
解析代码如下
/**
* 时标CP56Time2a解析
*
* @param b 时标CP56Time2a(长度为7 的int数组)
* @return 解析结果
*/
public static String TimeScale(int b[]) {
StringBuilder result = new StringBuilder();
int year = b[6] & 0x7F;
int month = b[5] & 0x0F;
int day = b[4] & 0x1F;
int week = (b[4] & 0xE0) / 32;
int hour = b[3] & 0x1F;
int minute = b[2] & 0x3F;
int second = (b[1] << 8) + b[0];
result.append("时标CP56Time2a:20");
result.append(year).append("-");
result.append(String.format("%02d", month)).append("-");
result.append(String.format("%02d", day)).append(",");
result.append(hour).append(":").append(minute).append(":");
result.append(second / 1000 + "." + second % 1000).append("\n");
return result.toString();
}
- 遥测量的解析
在101的遥测中定义到有三种类型的值,分别是归一化值,标度值,短浮点数,具体的定义大家可以参考“电力101/104规约中遥测量类型转换”,下面我说一下在Java中怎么解析这三种类型的值。直接上代码了。
public class Main {
public static void main(String[] args) {
Telemetry telemetry = new Telemetry();
System.out.println(telemetry.Bytes2Float_NVA(0xa4, 0x6a));// 报文数据的为A4 6A(低位在前,高位在后) ,为0x6AA4
System.out.println(telemetry.Bytes2Float_SVA(0xa4, 0x6a));// 报文数据的为A4 6A(低位在前,高位在后) ,为0x6AA4
System.out.println(telemetry.Bytes2Float_IEEE754("3e0779a6"));
//报文数据 A6 79 07 3E = 0x3e0779a6 = 0.1323(IEEE754 短浮点数)http://lostphp.com/hexconvert/
}
}
/**
* 遥测量解析
*
* @author zhangyu
*/
public class Telemetry {
/**
* 归一化值
*/
public static float Bytes2Float_NVA(int low, int high) {
float fVal;
int nva = (high << 8) + low;
// 符号位1位,0为正数,1为负数,后面的为补码表示,
// 正数的补码和原码相同不需要转换。对于负数,先取反码再加1得到补码的补码,就是原码了。
int symbol = (high & 0x80);//符号位:0表示正数,1表示负数
if (symbol == 0x80)
fVal = -1 * (((nva ^ 0xffff) + 1) & 0x7fff);// (nva ^ 0xffff) + 1 :补码的补码 ,取出后面的15位数据乘上-1得到值
else fVal = nva;
return fVal / 32767;//这里的32767值的是一个量纲,一般这个参数是主站和从站商定的
}
/**
* 标度化值
*/
public static float Bytes2Float_SVA(int low, int high) {
float fVal;
int nva = (high << 8) + low;
int symbol = (high & 0x80);//0表示正数,1表示负数
if (symbol == 0x80)
fVal = -1 * (((nva ^ 0xffff) + 1) & 0x7fff);
else fVal = nva;
return fVal;
}
/**
* 短浮点数
*
* @param data 从低位到高位按顺序
* @return
*/
public static float Bytes2Float_IEEE754(String data) {
return Float.intBitsToFloat(Integer.valueOf(data, 16));
}
}
对于短浮点数的计算,在Java的文档里有这样的说明。
还有一些ASDU里的信息体的解析过程代码,有点多,就不一一贴出来了
代码已经发布到GitHub,需要的伙伴可以直接下载
5.104规约解析遇到的一些难题
- 报文类型的区分
101报文区分定长和变长两种,而104规约都是变长的报文。但是对于104来说,其控制域决定了104报文分别属于三种类型(I帧S帧U帧)。所以在解析104到控制域的时候,是在解析104的第一个难题。我先说一下我自己的区分逻辑。
- 我是用控制域的第一字节去和3做位运算(因为和3做位运算就可以得到第一字节的D1/D0两位),从上面的图中我们可以看到,结果如果是1的话就是S格式,如果是3的话就是U格式,其他 的结果就是I格式的报文。
- 第二种方式是我在和一个网友交谈中得到的,他是运用奇数或者偶数来区分的,这里我列一个表格 ,这样的话如果第一个字节是偶数就是I帧,否则判断第三字节是0就是U帧。这样也可以区分报文的类型
但是我认为还是做位运算效率会更高,计算奇偶性还需要用到%这样的运算。
类型 | 第一字节 | 第二字节 | 第三字节 | 第四字节 |
---|---|---|---|---|
S帧 | 奇(1) | 0 | 偶 | — |
I帧 | 偶 | — | 偶 | — |
U帧 | 奇 | 0 | 0 | 0 |
- 位运算的运用
在解析规约的时候,我一般都是运用的位运算(&),这样既可以提高程序的效率,也有助于理解。首先在规约的定义中有很多的一个字节中的不同位代表一个含义,这种时候就运用到了位运算,这样就可以直接将对应位的值提取出来,例如那可变结构限定词这个字节来举个例子。最高位的D7代表的是信息元素地址的连续性,其中0不连续1连续,所以我们只需要拿这个字节和128(0x80)做位运算就可以得到D7这一位,如果结果是10000000就说明是1连续。这个地方更直观一点的判断方法就是((B&0x80)>>7)==1?连续 : 不连续。
- 时标的解析
首先时标这里一共有7个字节,下面对这几个字节的含义以及解析方法做一下解释:
- 年(byte 7):这里只有后面的7位有效,但是第8位填充的是0,所以这一个字节直接转换形成int就可以了。
- 月(byte 6):同上
- 小时(byte 4):同上
- 分钟(byte 3):同上
- 日(byte 5[bit 1~5]):第五字节的后5位表示的是日,byte5 & 0x1F 就可以得到(0x1F = 0001 1111),例如第五字节是87(87 = 0x57 = 0101 0111),其中010指的是星期,10111指的是day。这里使用87 & 0x1F就可以得到23。使用87 & 0xE0就可以得到64,然后再用这个结果除以32就可以得到结果,如果说除以32不太理解的话,也可以用结果右移运算64>>5也可以得到结果 。
- 毫秒(byte 1 and byte 2):这个地方首先是低前高后的问题(这个为题不太明白的可以先翻一下下面的第四个问题),( byte 2 << 8 ) + byte 1 ,这个地方我用一个表格说明一下(例如第一字节是6E,第二字节是2A)。
/** * 时标CP56Time2a解析 */
public static String TimeScale(int b[]) {
String str = "";
int year = b[6] & 0x7F;
int month = b[5] & 0x0F;
int day = b[4] & 0x1F;
int week = (b[4] & 0xE0) / 32;
// int week = (b[4] & 0xE0) >> 5;
int hour = b[3] & 0x1F;
int minute = b[2] & 0x3F;
int second = (b[1] << 8) + b[0];
str += "时标CP56Time2a:" + "20" + year + "-"
+ String.format("%02d", month) + "-"
+ String.format("%02d", day) + "," + hour + ":" + minute + ":"
+ second / 1000 + "." + second % 1000;
return str + "\n";
}
/** * 时间转16进制字符串 */
public static String date2HStr(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
StringBuilder builder = new StringBuilder();
String milliSecond = String.format("%04X", (calendar.get(Calendar.SECOND) * 1000) + calendar.get(Calendar.MILLISECOND));
builder.append(milliSecond.substring(2, 4));
builder.append(milliSecond.substring(0, 2));
builder.append(String.format("%02X", calendar.get(Calendar.MINUTE) & 0x3F));
builder.append(String.format("%02X", calendar.get(Calendar.HOUR_OF_DAY) & 0x1F));
int week = calendar.get(Calendar.DAY_OF_WEEK);
if (week == Calendar.SUNDAY)
week = 7;
else week--;
builder.append(String.format("%02X", (week << 5) + (calendar.get(Calendar.DAY_OF_MONTH) & 0x1F)));
builder.append(String.format("%02X", calendar.get(Calendar.MONTH) + 1));
builder.append(String.format("%02X", calendar.get(Calendar.YEAR) - 2000));
return builder.toString();
}
- 低前高后的问题
这里讲一下低前高后的问题,拿这个104规约的信息对象地址举例。例如34 12 00这是原报文的字节,再具体解析的时候我们要把它转成0x001234=4660,其实这里和上面的毫秒的解析方式一样。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/151343.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...