华为云编解码插件教程

一、 环境搭建

JDK 1.8以上

eclipse

Maven

具体说明https://support.huaweicloud.com/devg-IoT/iot_02_4020.html

二、 Profile说明

Profile实际上是一系列关于设备模型的描述文件,每个文件都使用JSON格式(键值对)。

Profile中首先需要说明设备的基本信息,包括厂商ID,厂商名称,设备类型,接入协议,以及设备可以提供的哪些服务等;其次,profile中要针对每一项服务,用一个独立的文件进行详细描述。服务,可以理解为是对设备消息(上下行)功能的一个分类,一个服务就代表一类功能;每个服务下包含若干属性和命令,每个属性对应上报消息中的某一个数据,每个命令字段则对应下行消息中的某些字段。

可选在线开发,离线开发

官方说明https://support.huaweicloud.com/devg-IoT/iot_02_9991.html

三、 编解码插件编写

从华为资源中心下载编解码插件Demo,并解压到本地。文件结构如下图所示:

image.png

下载地址https://developer.obs.cn-north-4.myhuaweicloud.com/manage/tool/CodecDemo/CodecDemo.zip

img

源代码在src文件夹下;编译生成的插件包在target文件夹下。src 文件夹包含 main 、test 两个子文件夹,main下存放源码,test下是单元测试代码。官网下载的Demo中,源码的路径是:src\main\java\com\Huawei\NBIoTDevice\WaterMeter,单元测试代码的路径是:src\test\java\com\Huawei\NBIoTDevice\WaterMeter。

插件源码文件有5个:

image.png

1、源文件说明

a) ProtocolAdapterImpl.java 可以理解为是插件的入口文件,对外提供调用接口。该文件只需要修改两个字符串的定义即可:

1
2
3
4
5
6
7
// 厂商名称

private static final String MANU_FACTURERID = "Huawei";

// 设备型号

private static final String MODEL = "NBIoTDevice";

修改为profile当中定义的厂商ID和设备型号。

b) CmdProcess.java 实现下行命令的编码工作,将从收到的服务器报文中提取出命令字段对应的内容,并将其转换成字节流。需要实现的函数是: public byte[] toByte()。

c) ReportProcess.java 实现将收到的二进制码流按照格式解码出对应profile中的属性值,并生成JSON格式。需要实现的函数是:

public ReportProcess(byte[] binaryData),根据二进制码流的格式,从中取出对应字节,转换成profile中对应属性的值。

public ObjectNode toJsonNode(),将解码出来的属性值封装成JSON格式。

d) ByteBufUtils.java 和 Utilty.java文件封装了一些公共方法,不用做修改。也不会使用到。

2、修改文件路径(包名)

插件包名的要求是:com.厂商名称.设备型号.设备类型。因此下载下来的代码,要根据自己的设备修改下文件路径。即将Huawei文件夹重命名为profile中定义的厂商名称,NBIoTDevice文件夹重命名为profile中定义的设备型号,WaterMeter文件夹重命名为profile中定义的设备类型。注意:src\main 和src\test 下都要修改。在本例中,需要修改为:

1
2
src\main\java\com\ThirdParty\MyModel\MyTyp,
src\test\java\com\ThirdParty\MyModel\MyType

3、修改pom.xml

打开pom.xml文件,修改第7行“artifactId”和第88行“Bundle-SymbolicName”的值为:设备类型-厂商ID-设备型号。在本例中,需要修改为:MyType-ThirdParty-MyModel。

4. 导入工程后是有错误的。

这是因为我们在第2节中将文件路径修改了,与代码里面的包路径不一致引起的。解决方法为:依次打开源文件,将第一行的

1
2
3
4
5
package com.Huawei.NBIoTDevice.WaterMeter;

修改为

package com.ThirdParty.MyModel.MyType;

打开OSGI_INF目录下的CodeProvideHandler.xml 文件:

image.png

CodeProvideHandler.xml路径

打开后,文件内容如下图所示:

image.png

CodeProvideHandler.xml内容

将Name 、 Class* 内的路径也修改为对应的包路径:

image.png

CodeProvideHandler.xml修改

5、代码实现

1)修改ProtocolAdatpterImpl.java文件

在文件中找到如下两行:

1
2
3
4
// 厂商名称
private static final String MANU_FACTURERID = "Huawei";
// 设备型号
private static final String MODEL = "NBIoTDevice";

MANU_FACTURERIDMODEL定义修改为profile中定义的厂商ID和设备型号,本例中需要修改为:

1
2
3
4
// 厂商名称
private static final String MANU_FACTURERID = "ThirdParty";
// 设备型号
private static final String MODEL = "MyModel";
2)解码实现

解码,是将NB模组上报的二进制码流按格式解析出对应字段的过程。解码的代码在ReportProcess.java 文件中。

第一个函数:public ReportProcess(byte[] binaryData) 入参 byte[] binaryData就是NB模组上报的二进制码流。解码得到数据存储在成员变量当中。本例中的代码实现如下:

a.先看官方提供的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
/**
* @param binaryData 设备发送给平台coap报文的payload部分
* 本例入参:AA 72 00 00 32 08 8D 03 20 62 33 99
* byte[0]--byte[1]: AA 72 命令头
* byte[2]: 00 mstType 00表示设备上报数据deviceReq
* byte[3]: 00 hasMore 0表示没有后续数据,1表示有后续数据,不带按照0处理
* byte[4]--byte[11]:服务数据,根据需要解析//如果是deviceRsp,byte[4]表示是否携带mid, byte[5]--byte[6]表示短命令Id
* @return
*/
public ReportProcess(byte[] binaryData) {
// identifier参数可以根据入参的码流获得,本例指定默认值123
// identifier = "123";

/*
如果是设备上报数据,返回格式为
{
"identifier":"123",
"msgType":"deviceReq",
"hasMore":0,
"data":[{"serviceId":"Brightness",
"serviceData":{"brightness":50},
{
"serviceId":"Electricity",
"serviceData":{"voltage":218.9,"current":800,"frequency":50.1,"powerfactor":0.98},
{
"serviceId":"Temperature",
"serviceData":{"temperature":25},
]
}
*/
if (binaryData[2] == bDeviceReq) {
msgType = "deviceReq";
hasMore = binaryData[3];

//serviceId=Brightness 数据解析
brightness = binaryData[4];

//serviceId=Electricity 数据解析
////16进制转二进制然后移位,这里需要注意一下,我算了好久,emmm
voltage = (double) (((binaryData[5] << 8) + (binaryData[6] & 0xFF)) * 0.1f);
current = (binaryData[7] << 8) + binaryData[8];
powerfactor = (double) (binaryData[9] * 0.01);
frequency = (double) binaryData[10] * 0.1f + 45;

//serviceId=Temperature 数据解析
temperature = (int) binaryData[11] & 0xFF - 128;
}
}

public ObjectNode toJsonNode() {
try {
//组装body体
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();

// root.put("identifier", this.identifier);
root.put("msgType", this.msgType);

//根据msgType字段组装消息体
if (this.msgType.equals("deviceReq")) {
root.put("hasMore", this.hasMore);
ArrayNode arrynode = mapper.createArrayNode();

//serviceId=Brightness 数据组装
ObjectNode brightNode = mapper.createObjectNode();
brightNode.put("serviceId", "Brightness");
ObjectNode brightData = mapper.createObjectNode();
brightData.put("brightness", this.brightness);
brightNode.put("serviceData", brightData);
arrynode.add(brightNode);
//serviceId=Electricity 数据组装
ObjectNode electricityNode = mapper.createObjectNode();
electricityNode.put("serviceId", "Electricity");
ObjectNode electricityData = mapper.createObjectNode();
electricityData.put("voltage", this.voltage);
electricityData.put("current", this.current);
electricityData.put("frequency", this.frequency);
electricityData.put("powerfactor", this.powerfactor);
electricityNode.put("serviceData", electricityData);
arrynode.add(electricityNode);
//serviceId=Temperature 数据组装
ObjectNode temperatureNode = mapper.createObjectNode();
temperatureNode.put("serviceId", "Temperature");
ObjectNode temperatureData = mapper.createObjectNode();
temperatureData.put("temperature", this.temperature);
temperatureNode.put("serviceData", temperatureData);
arrynode.add(temperatureNode);

//serviceId=Connectivity 数据组装
ObjectNode ConnectivityNode = mapper.createObjectNode();
ConnectivityNode.put("serviceId", "Connectivity");
ObjectNode ConnectivityData = mapper.createObjectNode();
ConnectivityData.put("signalStrength", 5);
ConnectivityData.put("linkQuality", 10);
ConnectivityData.put("cellId", 9);
ConnectivityNode.put("serviceData", ConnectivityData);
arrynode.add(ConnectivityNode);

//serviceId=battery 数据组装
ObjectNode batteryNode = mapper.createObjectNode();
batteryNode.put("serviceId", "battery");
ObjectNode batteryData = mapper.createObjectNode();
batteryData.put("batteryVoltage", 25);
batteryData.put("battervLevel", 12);
batteryNode.put("serviceData", batteryData);
arrynode.add(batteryNode);

root.put("data", arrynode);

} else {
root.put("errcode", this.errcode);
//此处需要考虑兼容性,如果没有传mid,则不对其进行解码
if (isContainMid) {
root.put("mid", this.mid);//mid
}
//组装body体,只能为ObjectNode对象
ObjectNode body = mapper.createObjectNode();
body.put("result", 0);
root.put("body", body);
}
return root;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

b.太长了,看我写的简陋的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public ReportProcess(byte[] binaryData) {
/*
设备上报数据格式为 2020313939
ACII码解码: 199
*/
msgType = "deviceReq"; ////设备上报标志
light=0;
for (int i=0;i<5;i++) {
if (binaryData[i]==20) {
binaryData[i]=00;
}
else {
binaryData[i]=(byte) ((int)binaryData[i]&0x0f);
}
}
light =(int) (((int)binaryData[0]*10000)+((int)binaryData[1]*1000)+((int)binaryData[2]*100)+((int)binaryData[3]*10)+((int)binaryData[4]));
}
public ObjectNode toJsonNode() {
try {
//组装body体
//注意serviceId 对应好
/*public ObjectNode toJsonNode() 返回一个ObjectNode对象(JSON)。
该函数的功能,是将解码后得到的数据,按照规定格式填入一个JSON对象中。
*/
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
root.put("msgType", "deviceReq");
ArrayNode arrynode = mapper.createArrayNode();

ObjectNode serviceNode = mapper.createObjectNode();
ObjectNode serviceDataNode = mapper.createObjectNode();
serviceDataNode.put("Light", this.light);
serviceNode.put("serviceId", "Light");
serviceNode.set("serviceData", serviceDataNode);
arrynode.add(serviceNode);

root.set("data", arrynode);
return root;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/*JSON对象的内容格式要求是:

"msgType": "deviceReq", 表示设备上报数据,固定不动;“data”:数组对象,数组中的每个元素分别对应profile中的一个服务;“serviceID”的值是profile中定义的服务名称;“serviceData”的值是该服务下所有的属性值。

该函数代码比较简单,主要是用到了 ObjectMapper 这个类,该类提供了JAVA中操作JSON数据的方法
*/
3)编码实现

修改CmdProcess.java中的代码,实现插件对下发命令和上报数据响应的编码能力。

a.同样,先看官方提供的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public CmdProcess(ObjectNode input) {

try {
// this.identifier = input.get("identifier").asText();
this.msgType = input.get("msgType").asText();
/*
平台收到设备上报消息,编码ACK
{
"identifier":"0",
"msgType":"cloudRsp",
"request": ***,//设备上报的码流
"errcode":0,
"hasMore":0
}
* */
if (msgType.equals("cloudRsp")) {
//在此组装ACK的值
this.errcode = input.get("errcode").asInt();
this.hasMore = input.get("hasMore").asInt();
} else {
/*
平台下发命令到设备,输入
{
"identifier":0,
"msgType":"cloudReq",
"serviceId":"WaterMeter",
"cmd":"SET_DEVICE_LEVEL",
"paras":{"value":"20"},
"hasMore":0

}
* */
//此处需要考虑兼容性,如果没有传mId,则不对其进行编码
if (input.get("mid") != null) {
this.mid = input.get("mid").intValue();
}
this.cmd = input.get("cmd").asText();
this.paras = input.get("paras");
this.hasMore = input.get("hasMore").asInt();
}

} catch (Exception e) {
e.printStackTrace();
}
}
public byte[] toByte() {
try {
if (this.msgType.equals("cloudReq")) {
/*
应用服务器下发的控制命令,本例只有一条控制命令:SET_DEVICE_LEVEL
如果有其他控制命令,增加判断即可。
* */
if (this.cmd.equals("SET_DEVICE_LEVEL")) {
int brightlevel = paras.get("value").asInt();
byte[] byteRead = new byte[5];
ByteBufUtils buf = new ByteBufUtils(byteRead);
buf.writeByte((byte) 0xAA);
buf.writeByte((byte) 0x72);
buf.writeByte((byte) brightlevel);

//此处需要考虑兼容性,如果没有传mId,则不对其进行编码
if (Utilty.getInstance().isValidofMid(mid)) {
byte[] byteMid = new byte[2];
byteMid = Utilty.getInstance().int2Bytes(mid, 2);
buf.writeByte(byteMid[0]);
buf.writeByte(byteMid[1]);
}

return byteRead;
}
}

/*
平台收到设备的上报数据,根据需要编码ACK,对设备进行响应,如果此处返回null,表示不需要对设备响应。
* */
else if (this.msgType.equals("cloudRsp")) {
byte[] ack = new byte[4];
ByteBufUtils buf = new ByteBufUtils(ack);
buf.writeByte((byte) 0xAA);
buf.writeByte((byte) 0xAA);
buf.writeByte((byte) this.errcode);
buf.writeByte((byte) this.hasMore)
return ack;
}
return null;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
return null;
}
}
}

b.我写的简陋的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public byte[] toByte() {
try {
if (this.msgType.equals("cloudReq")) {
/*
应用服务器下发的控制命令,本例只有一条控制命令:Control
如果有其他控制命令,增加判断即可。
* */
/*
平台下发命令到设备,输入
{
"identifier":0,
"msgType":"cloudReq",
"serviceId":"Light",
"cmd":"Control",
"mid":0,
"paras":{"LED":"ON"},
"hasMore":0
}
*/
if (this.cmd.equals("Control")) {
byte[] downData = new byte[4];
String Data = paras.get("LED").asText();
downData = Data.getBytes();

//此处需要考虑兼容性,如果没有传mId,则不对其进行编码
if (Utilty.getInstance().isValidofMid(mid)) {
byte[] byteMid = new byte[2];
byteMid = Utilty.getInstance().int2Bytes(mid, 2);
buf.writeByte(byteMid[0]);
buf.writeByte(byteMid[1]);
}
return downData;
}
}
/*
平台收到设备的上报数据,根据需要编码ACK,对设备进行响应,如果此处返回null,表示不需要对设备响应。
* */
else if (this.msgType.equals("cloudRsp")) {
byte[] ack = new byte[4];
ByteBufUtils buf = new ByteBufUtils(ack);
buf.writeByte((byte) 0xAA);
buf.writeByte((byte) 0xAA);
buf.writeByte((byte) this.errcode);
buf.writeByte((byte) this.hasMore);
return ack;
}
return null;
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
return null;
}
}
/**
"msgType": "cloudReq", 固定值,表示服务器下行命令;

"serviceId",profile中对应的服务,本例中是"Transmission",

"cmd",profile中定义的下行命令,本例中是"CLOUDREQ",

"paras",profile中定义的下行命令的各个字段
**/

6. 编解码插件打包 (以官网代码为例)

插件变成完成后,需要使用Maven打包成jar包,并制作成插件包。

Maven打包
  1. 打开DOS窗口,进入“pom.xml”所在的目录。

  2. 输入maven打包命令:mvn package。

  3. DOS窗口中显示“BUILD SUCCESS”后,打开与“pom.xml”目录同级的target文件夹,获取打包好的jar包。

jar包命名规范为:设备类型-厂商ID-设备型号-版本.jar,例如:WaterMeter-Huawei-NBIoTDevice-version.jar。

点击放大

  • com目录存放的是class文件。
  • META-INF下存放的是OSGI框架下的jar的描述文件(根据pom.xml配置生成的)。
  • OSGI-INF下存放的是服务配置文件,把编解码注册为服务,供平台调用(只能有一个xml文件)。
  • 其他jar是编解码引用到的jar包。
制作插件包
  1. 新建文件夹命名为“package”,包含一个“preload/”子文件夹。

  2. 将打包好的jar包放到“preload/”文件夹。

点击放大

  1. 在“package”文件夹中,新建“package-info.json”文件。该文件的字段说明和模板如下:

    说明:

    “package-info.json”需要以UTF-8无BOM格式编码。仅支持英文字符。

    字段名 字段描述 是否必填
    specVersion 描述文件版本号,填写固定值:”1.0”。
    fileName 软件包文件名,填写固定值:”codec-demo”
    version 软件包版本号。描述package.zip的版本,请与下面的bundleVersion取值保持一致。
    deviceType 设备类型,与Profile文件中的定义保持一致。
    manufacturerName 制造商名称,与Profile文件中的定义保持一致,否则无法上传到平台。
    model 产品型号,与Profile文件中的定义保持一致。
    platform 平台类型,本插件包运行的物联网平台的操作系统,填写固定值:”linux”。
    packageType 软件包类型,该字段用来描述本插件最终部署的平台模块,填写固定值:”CIGPlugin”。
    date 出包时间,格式为:”yyyy-MM-dd HH-mm-ss”,如”2017-05-06 20:48:59”。
    description 对软件包的自定义描述。
    ignoreList 忽略列表,默认为空值。
    bundles 一组bundle的描述信息。说明:bundle就是压缩包中的jar包,只需要写一个bundle。
    字段名 字段描述 是否必填
    bundleName 插件名称,和上文中pom.xml的Bundle-SymbolicName保持一致。
    bundleVersion 插件版本,与上面的version取值保持一致。
    priority 插件优先级,可赋值默认值:5。
    fileName 插件jar的文件名称。
    bundleDesc 插件描述,用来介绍bundle功能。
    versionDesc 插件版本描述,用来介绍版本更迭时的功能特性。

    package-info.json文件模板:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    { 
    "specVersion":"1.0",
    "fileName":"codec-demo",
    "version":"1.0.0",
    "deviceType":"WaterMeter",
    "manufacturerName":"Huawei",
    "model":"NBIoTDevice",
    "description":"codec",
    "platform":"linux",
    "packageType":"CIGPlugin",
    "date":"2017-02-06 12:16:59",
    "ignoreList":[],
    "bundles":[
    {
    "bundleName": "WaterMeter-Huawei-NBIoTDevice",
    "bundleVersion": "1.0.0",
    "priority":5,
    "fileName": "WaterMeter-Huawei-NBIoTDevice-1.0.0.jar",
    "bundleDesc":"",
    "versionDesc":""
    }]
    }
  2. 选中“package”文件夹中的全部文件,打包成zip格式(“package.zip”)。

说明:

“package.zip”中不能包含“package”这层目录。

7.编解码插件质检

编解码插件的质检用于检验编解码是否可以正常使用。

  1. 获取编解码插件检测工具

  2. 将检测工具“pluginDetector.jar”、Profile文件的“devicetype-capability.json”和需要检测的编解码插件包“package.zip”和tool文件夹放在同一个目录下。

img

比较简单,具体参照 官方教程

8.编解码插件包离线签名

具体参照 [官方教程](https://support.huaweicloud.com/devg-IoT/iot_02_4020.html)

四、 附上我的Code

https://github.com/Xbean1028/NB-IOT

第一次学习,请多指教