華爲雲編解碼插件教程
一、 環境搭建
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,並解壓到本地。文件結構如下圖所示:
下載地址https://developer.obs.cn-north-4.myhuaweicloud.com/manage/tool/CodecDemo/CodecDemo.zip
源代碼在src文件夾下;編譯生成的插件包在target文件夾下。src 文件夾包含 main 、test 兩個子文件夾,main下存放源碼,test下是單元測試代碼。官網下載的Demo中,源碼的路徑是:src\main\java\com\Huawei\NBIoTDevice\WaterMeter,單元測試代碼的路徑是:src\test\java\com\Huawei\NBIoTDevice\WaterMeter。
插件源碼文件有5個:
1、源文件說明
a) ProtocolAdapterImpl.java 可以理解爲是插件的入口文件,對外提供調用接口。該文件只需要修改兩個字符串的定義即可:
// 廠商名稱
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 下都要修改。在本例中,需要修改爲:
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節中將文件路徑修改了,與代碼裏面的包路徑不一致引起的。解決方法爲:依次打開源文件,將第一行的
package com.Huawei.NBIoTDevice.WaterMeter;
修改爲
package com.ThirdParty.MyModel.MyType;
打開OSGI_INF目錄下的CodeProvideHandler.xml 文件:
CodeProvideHandler.xml路徑
打開後,文件內容如下圖所示:
CodeProvideHandler.xml內容
將Name 、 Class* 內的路徑也修改爲對應的包路徑:
CodeProvideHandler.xml修改
5、代碼實現
1)修改ProtocolAdatpterImpl.java文件
在文件中找到如下兩行:
// 廠商名稱
private static final String MANU_FACTURERID = "Huawei";
// 設備型號
private static final String MODEL = "NBIoTDevice";
將MANU_FACTURERID 和 MODEL定義修改爲profile中定義的廠商ID和設備型號,本例中需要修改爲:
// 廠商名稱
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.先看官方提供的例子:
/**
* @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.太長了,看我寫的簡陋的例子
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.同樣,先看官方提供的例子:
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.我寫的簡陋的例子
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打包
-
打開DOS窗口,進入“pom.xml”所在的目錄。
-
輸入maven打包命令:mvn package。
-
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包。
製作插件包
-
新建文件夾命名爲“package”,包含一個“preload/”子文件夾。
-
將打包好的jar包放到“preload/”文件夾。
-
在“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文件模板:
{ "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":"" }] }
-
選中“package”文件夾中的全部文件,打包成zip格式(“package.zip”)。
說明:
“package.zip”中不能包含“package”這層目錄。
7.編解碼插件質檢
編解碼插件的質檢用於檢驗編解碼是否可以正常使用。
-
獲取編解碼插件檢測工具。
-
將檢測工具“pluginDetector.jar”、Profile文件的“devicetype-capability.json”和需要檢測的編解碼插件包“package.zip”和tool文件夾放在同一個目錄下。
比較簡單,具體參照 官方教程
8.編解碼插件包離線簽名
具體參照 官方教程
四、 附上我的Code
https://github.com/Xbean1028/NB-IOT
第一次學習,請多指教