華爲雲離線編解碼插件教程

華爲雲編解碼插件教程

一、 環境搭建

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 可以理解爲是插件的入口文件,對外提供調用接口。該文件只需要修改兩個字符串的定義即可:

// 廠商名稱

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 文件:

image.png

CodeProvideHandler.xml路徑

打開後,文件內容如下圖所示:

image.png

CodeProvideHandler.xml內容

將Name 、 Class* 內的路徑也修改爲對應的包路徑:

image.png

CodeProvideHandler.xml修改

5、代碼實現

1)修改ProtocolAdatpterImpl.java文件

在文件中找到如下兩行:

// 廠商名稱
private static final String MANU_FACTURERID = "Huawei";
// 設備型號
private static final String MODEL  = "NBIoTDevice";

MANU_FACTURERIDMODEL定義修改爲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打包
  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/”文件夾。

    點擊放大

  3. 在“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":"" 
        }] 
    }
    
  4. 選中“package”文件夾中的全部文件,打包成zip格式(“package.zip”)。

    說明:

    “package.zip”中不能包含“package”這層目錄。

7.編解碼插件質檢

編解碼插件的質檢用於檢驗編解碼是否可以正常使用。

  1. 獲取編解碼插件檢測工具

  2. 將檢測工具“pluginDetector.jar”、Profile文件的“devicetype-capability.json”和需要檢測的編解碼插件包“package.zip”和tool文件夾放在同一個目錄下。

    img

    比較簡單,具體參照 官方教程

8.編解碼插件包離線簽名

​ 具體參照 官方教程

四、 附上我的Code

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

第一次學習,請多指教

發佈了52 篇原創文章 · 獲贊 21 · 訪問量 8246
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章