網(wǎng)關(guān)與子設(shè)備
網(wǎng)關(guān)設(shè)備及其下子設(shè)備接入物聯(lián)網(wǎng)平臺時,需要使用子設(shè)備管理功能。 本文介紹如何配置和開發(fā)網(wǎng)關(guān)子設(shè)備接入物聯(lián)網(wǎng)平臺。
功能說明
網(wǎng)關(guān)與子設(shè)備管理功能提供了子設(shè)備動態(tài)注冊、獲取云端網(wǎng)關(guān)子設(shè)備列表、添加子設(shè)備、刪除子設(shè)備、子設(shè)備上線、子設(shè)備離線、監(jiān)聽子設(shè)備禁用和刪除、代理子設(shè)備上下行通信的能力。網(wǎng)關(guān)本身是一個直連設(shè)備可直接使用上述所有能力。網(wǎng)關(guān)和子設(shè)備之間的連接、數(shù)據(jù)通信需要用戶處理。
開發(fā)說明
網(wǎng)關(guān)開發(fā)過程說明
廠商在物聯(lián)網(wǎng)平臺定義網(wǎng)關(guān)產(chǎn)品時,設(shè)置節(jié)點類型為網(wǎng)關(guān)設(shè)備,設(shè)置網(wǎng)關(guān)的身份認證方式,并根據(jù)網(wǎng)關(guān)功能定義Topic或物模型,并參照前面的章節(jié)中的說明對網(wǎng)關(guān)自身的功能進行開發(fā)。
實現(xiàn)子設(shè)備的管理功能:
子設(shè)備的發(fā)現(xiàn)與連接功能。
由廠商自行實現(xiàn),阿里不提供網(wǎng)關(guān)如何發(fā)現(xiàn)以及如何將子設(shè)備連接到網(wǎng)關(guān)的代碼實現(xiàn)。
子設(shè)備證書的獲取方式。
下文介紹的獲取子設(shè)備證書的方式可供廠商參考。
物聯(lián)網(wǎng)平臺遠程管理網(wǎng)關(guān)下子設(shè)備。
當網(wǎng)關(guān)發(fā)現(xiàn)并接入子設(shè)備后,需要調(diào)用SDK的添加子設(shè)備接口通知物聯(lián)網(wǎng)平臺:先獲得該子設(shè)備的證書信息,然后調(diào)用SDK提供的子設(shè)備上線接口通知物聯(lián)網(wǎng)平臺。
說明如果一個子設(shè)備處于離線狀態(tài),物聯(lián)網(wǎng)平臺遠程控制子設(shè)備時,會直接返回失敗,不會將命令發(fā)送給網(wǎng)關(guān)再等待錯誤提示或者超時提示。
同步子設(shè)備運行狀態(tài)到物聯(lián)網(wǎng)平臺。
當網(wǎng)關(guān)通知物聯(lián)網(wǎng)平臺一個子設(shè)備上線后,需要將子設(shè)備的狀態(tài)信息上報云端,以保證子設(shè)備在云端的狀態(tài)與當前子設(shè)備的狀態(tài)一致。
說明如果使用物模型定義子設(shè)備功能時,子設(shè)備上線需要將屬性的最新數(shù)值上報到物聯(lián)網(wǎng)平臺。
當網(wǎng)關(guān)代理接入物聯(lián)網(wǎng)平臺的子設(shè)備離線時,網(wǎng)關(guān)需要調(diào)用子設(shè)備離線接口,通知物聯(lián)網(wǎng)平臺該子設(shè)備已離線。
當在線子設(shè)備的屬性發(fā)生變化時,也需要實時上報到物聯(lián)網(wǎng)平臺。
當網(wǎng)關(guān)離線并再次上線時(例如網(wǎng)絡連接斷開或網(wǎng)關(guān)重啟),網(wǎng)關(guān)需要對所有已添加到物聯(lián)網(wǎng)平臺的子設(shè)備,再次調(diào)用添加子設(shè)備接口和子設(shè)備上線接口。如果網(wǎng)關(guān)不知道子設(shè)備的屬性與網(wǎng)關(guān)離線前上報到物聯(lián)網(wǎng)平臺的是否一致,則網(wǎng)關(guān)需要將子設(shè)備的最新屬性再次上報到物聯(lián)網(wǎng)平臺。
子設(shè)備接收物聯(lián)網(wǎng)平臺下發(fā)的消息。
當網(wǎng)關(guān)接收到物聯(lián)網(wǎng)平臺下發(fā)給子設(shè)備的控制消息時,網(wǎng)關(guān)如何將該消息轉(zhuǎn)換成子設(shè)備識別的格式并發(fā)送給子設(shè)備,由網(wǎng)關(guān)廠商進行實現(xiàn)。
子設(shè)備開發(fā)說明
設(shè)備廠商在物聯(lián)網(wǎng)平臺定義子設(shè)備產(chǎn)品,設(shè)置節(jié)點類型為網(wǎng)關(guān)子設(shè)備,設(shè)置產(chǎn)品的身份認證方式。
阿里云并不在子設(shè)備上提供任何SDK,因此網(wǎng)關(guān)如何發(fā)現(xiàn)子設(shè)備、如何連接子設(shè)備、網(wǎng)關(guān)如何發(fā)現(xiàn)子設(shè)備上線或者離線、網(wǎng)關(guān)如何將來自物聯(lián)網(wǎng)平臺的命令發(fā)送給子設(shè)備,均由網(wǎng)關(guān)廠商與子設(shè)備廠商定義協(xié)議并實現(xiàn)。
網(wǎng)關(guān)的認證與連接
獲取子設(shè)備證書
子設(shè)備是通過網(wǎng)關(guān)代理在阿里云物聯(lián)網(wǎng)平臺進行注冊的,注冊時需要使用到子設(shè)備的證書進行身份驗證。網(wǎng)關(guān)獲取子設(shè)備證書的方式如下,網(wǎng)關(guān)廠家可根據(jù)自己的實際情況進行選用:
網(wǎng)關(guān)從子設(shè)備獲取子設(shè)備證書。
由網(wǎng)關(guān)與子設(shè)備之間定義一套協(xié)議,當網(wǎng)關(guān)發(fā)現(xiàn)與連接子設(shè)備后,獲取到子設(shè)備的證書。阿里云并不提供參考協(xié)議實現(xiàn),該協(xié)議由網(wǎng)關(guān)廠商與子設(shè)備廠商自行定義與實現(xiàn)。
網(wǎng)關(guān)預置子設(shè)備的證書。
如果網(wǎng)關(guān)設(shè)備可預先獲取需要連接的子設(shè)備信息,并且網(wǎng)關(guān)提供了某種配置方式輸入子設(shè)備的證書信息,則可以通過該方式獲取子設(shè)備的證書。同樣,該功能由網(wǎng)關(guān)廠商實現(xiàn)。
網(wǎng)關(guān)通過動態(tài)注冊獲取子設(shè)備證書。
網(wǎng)關(guān)可以通過某種協(xié)議發(fā)現(xiàn)與連接子設(shè)備,并獲取到子設(shè)備的型號(model)以及唯一標識(例如SN、MAC地址),但并不知道子設(shè)備的DeviceSecret時,可通過以下步驟獲取:
子設(shè)備在阿里云物聯(lián)網(wǎng)平臺進行產(chǎn)品定義,云端會為子設(shè)備生成ProductKey。
網(wǎng)關(guān)設(shè)備建立子設(shè)備型號(model)到阿里云物聯(lián)網(wǎng)平臺ProductKey的映射(網(wǎng)關(guān)廠家在網(wǎng)關(guān)上實現(xiàn)該映射),并將設(shè)備的唯一標識作為阿里云物聯(lián)網(wǎng)平臺的DeviceName。
網(wǎng)關(guān)通過阿里云物聯(lián)網(wǎng)平臺提供的動態(tài)注冊功能,從云端獲取子設(shè)備的DeviceSecret,從而得到完整的子設(shè)備的證書信息。
使能網(wǎng)關(guān)模塊
在網(wǎng)關(guān)初始化時,將enableGateway
選項設(shè)置為true
。具體配置,請參見認證與連接中一機一密的ioTDMConfig.enableGateway
配置。
// 默認不開啟網(wǎng)關(guān)功能,開啟之后,初始化的時候會初始化網(wǎng)關(guān)模塊,獲取云端網(wǎng)關(guān)子設(shè)備列表
ioTDMConfig.enableGateway = true;
子設(shè)備動態(tài)注冊
使用ProductKey和DeviceName動態(tài)注冊
子設(shè)備添加到網(wǎng)關(guān)之前需要先進行動態(tài)注冊獲取子設(shè)備證書信息,您需要在物聯(lián)網(wǎng)平臺開啟動態(tài)注冊功能。動態(tài)注冊支持同時注冊多個子設(shè)備,用于獲取子設(shè)備證書。
如果本地已有子設(shè)備證書信息,可以跳過此步驟。如果子設(shè)備已經(jīng)被綁定到其他網(wǎng)關(guān)設(shè)備,動態(tài)注冊不會返回該子設(shè)備的證書信息。
LinkKit.getInstance().getGateway().gatewaySubDevicRegister(getSubDevList(), new IConnectSendListener() {
@Override
public void onResponse(ARequest aRequest, AResponse aResponse) {
ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + (aResponse == null ? "null" : aResponse.data) + "]");
try {
// 子設(shè)備動態(tài)注冊成功
ResponseModel<List<DeviceInfo>> response = JSONObject.parseObject(aResponse.data.toString(), new TypeReference<ResponseModel<List<DeviceInfo>>>() {
}.getType());
// 根據(jù) response 的數(shù)據(jù)判斷是否成功 code=200
//TODO 保存子設(shè)備的證書信息
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onFailure(ARequest aRequest, AError aError) {
// 子設(shè)備動態(tài)注冊失敗
}
});
使用ProductKey、DeviceName、ProductSecret動態(tài)注冊
如果當前已經(jīng)有子設(shè)備的ProductKey、DeviceName、ProductSecret信息,并且需要進行搶占式動態(tài)注冊,則可以使用該方式進行子設(shè)備動態(tài)注冊。搶占式動態(tài)注冊是指被其它網(wǎng)關(guān)設(shè)備綁定的子設(shè)備證書也會返回。
該動態(tài)注冊方式需要預先獲取子設(shè)備的ProductSecret,安全性會低于第一種動態(tài)注冊方式。
推薦通過物聯(lián)網(wǎng)平臺遠程配置下發(fā)(COTA)子設(shè)備的ProductKey、DeviceName、ProductSecret信息到網(wǎng)關(guān)設(shè)備,然后再進行搶占式動態(tài)注冊。
應用場景
需要將子設(shè)備從A網(wǎng)關(guān)綁定到B網(wǎng)關(guān)的場景。
代碼實現(xiàn)
// 該動態(tài)注冊方案需要提前知道子設(shè)備的productSecret,安全性會比下面一種子設(shè)備動態(tài)注冊低一點
// 這種動態(tài)注冊方式可以考慮和COTA-遠程配置下發(fā)配合使用,在云端下發(fā)子設(shè)備的ProductKey、DeviceName、ProductSecret,網(wǎng)關(guān)收到后
// 完成動態(tài)注冊
// 使用于需要搶占綁定關(guān)系時使用
MqttPublishRequest request = new MqttPublishRequest();
final RequestModel requestModel = new RequestModel();
requestModel.id = String.valueOf(IDGenerater.generateId());
requestModel.version = "1.0";
requestModel.method = GatewayChannel.METHOD_PRESET_SUBDEV_REGITER;
request.isRPC = true;
JSONObject jsonObject = new JSONObject();
JSONArray jsonArray = new JSONArray();
for (int i = 0; i < presetSubdevList.size(); i++) {
DeviceInfo itemDev = presetSubdevList.get(i);
Map<String, String> itemMap = new HashMap<>();
itemMap.put("productKey", itemDev.productKey);
itemMap.put("deviceName", itemDev.deviceName);
itemMap.put("random", RandomStringUtil.getRandomString(10));
String sign = SignUtils.hmacSign(itemMap, itemDev.productSecret);
itemMap.put("sign", sign);
itemMap.put("signMethod", "hmacsha1");
jsonArray.add(itemMap);
}
jsonObject.put("proxieds", jsonArray);
requestModel.params = jsonObject;
request.payloadObj = requestModel.toString();
LinkKit.getInstance().getGateway().subDevicRegister(request, new IConnectSendListener() {
@Override
public void onResponse(ARequest aRequest, AResponse aResponse) {
ALog.d(TAG, "onResponse() called with: aRequest = [" + aRequest + "], aResponse = [" + aResponse + "]");
try {
showToast("收到子設(shè)備動態(tài)結(jié)果");
ResponseModel<Map<String, List<DeviceInfo>>> responseModel = JSONObject.parseObject(aResponse.data.toString(),
new TypeReference<ResponseModel<Map<String, List<DeviceInfo>>>>() {
}.getType());
// TODO 保存子設(shè)備的證書信息
ALog.d(TAG, "onResponse responseModel=" + JSONObject.toJSONString(responseModel));
// {"code":200,"data":{"failures":[],"successes":[{"deviceSecret":"xxx","productKey":"xxx","deviceName":"xxx"}]},"id":"1","message":"success","method":"thing.proxy.provisioning.product_register","version":"1.0"}
// 動態(tài)注冊成功列表
List<DeviceInfo> successList = responseModel.data.get("successes");
// 動態(tài)注冊失敗列表
List<DeviceInfo> failList = responseModel.data.get("failures");
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onFailure(ARequest aRequest, AError aError) {
ALog.d(TAG, "onFailure() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]");
}
});
代理子設(shè)備接入
獲取子設(shè)備列表
獲取網(wǎng)關(guān)當前在物聯(lián)網(wǎng)平臺已經(jīng)有的子設(shè)備列表。
LinkKit.getInstance().getGateway().gatewayGetSubDevices(new IConnectSendListener() {
@Override
public void onResponse(ARequest aRequest, AResponse aResponse) {
// 獲取子設(shè)備列表結(jié)果
try {
ResponseModel<List<DeviceInfo>> response = JSONObject.parseObject(aResponse.data.toString(), new TypeReference<ResponseModel<List<DeviceInfo>>>() {
}.getType());
// TODO 根據(jù)實際應用場景處理
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onFailure(ARequest aRequest, AError aError) {
// 獲取子設(shè)備列表失敗
}
});
添加子設(shè)備
在獲取到子設(shè)備證書后,可以參考以下代碼添加網(wǎng)關(guān)與子設(shè)備的拓撲關(guān)系。
final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 證書:產(chǎn)品型號(必填)
deviceInfo.deviceName = deviceName; // 證書:設(shè)備標識 (必填)
LinkKit.getInstance().getGateway().gatewayAddSubDevice(deviceinfo, new ISubDeviceConnectListener() {
@Override
public String getSignMethod() {
// 使用的簽名方法
return "hmacsha1";
}
@Override
public String getSignValue() {
// 獲取簽名,用戶使用deviceSecret獲得簽名結(jié)果
Map<String, String> signMap = new HashMap<>();
signMap.put("productKey", info.productKey);
signMap.put("deviceName", info.deviceName);
// signMap.put("timestamp", String.valueOf(System.currentTimeMillis()));
signMap.put("clientId", getClientId());
return SignUtils.hmacSign(signMap, info.deviceSecret);
}
@Override
public String getClientId() {
// clientId 可為任意固定值,不可以是隨機值
return "id";
}
@Override
public void onConnectResult(boolean isSuccess, ISubDeviceChannel iSubDeviceChannel, AError aError) {
// 添加結(jié)果
if (isSuccess) {
// 子設(shè)備添加成功,接下來可以做子設(shè)備上線的邏輯
// subDevOnline(null);
}
}
@Override
public void onDataPush(String s, AMessage message) {
// 收到子設(shè)備下行數(shù)據(jù) topic=" + s + ", data=" + message
// 如禁用刪除已經(jīng)設(shè)置、服務調(diào)用等返回的數(shù)據(jù)message.data 是byte[]
}
});
刪除子設(shè)備
刪除網(wǎng)關(guān)下的子設(shè)備。
final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 產(chǎn)品型號(必填)
deviceInfo.deviceName = deviceName; // 設(shè)備標識 (必填)
LinkKit.getInstance().getGateway().gatewayDeleteSubDevice(deviceinfo, new ISubDeviceRemoveListener() {
@Override
public void onSuceess() {
// 成功刪除子設(shè)備
}
@Override
public void onFailed(AError aError) {
// 刪除子設(shè)備失敗
}
});
子設(shè)備上線
調(diào)用子設(shè)備上線接口前,請確保已完成子設(shè)備添加。
由于接口調(diào)用都是異步的,子設(shè)備上線接口不能在子設(shè)備添加的下一行調(diào)用,而是要放到子設(shè)備添加成功的回調(diào)中調(diào)用。
網(wǎng)關(guān)發(fā)現(xiàn)子設(shè)備連上網(wǎng)關(guān)后,需要參考如下代碼將子設(shè)備上線,子設(shè)備上線后可以執(zhí)行子設(shè)備的訂閱、發(fā)布等操作。
final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; //產(chǎn)品型號(必填)
deviceInfo.deviceName = deviceName; //設(shè)備標識(必填)
LinkKit.getInstance().getGateway().gatewaySubDeviceLogin(deviceinfo, new ISubDeviceActionListener() {
@Override
public void onSuccess() {
// 代理子設(shè)備上線成功
// 上線之后可訂閱刪除和禁用的下行通知
// subDevDisable(null);
// subDevDelete(null);
}
@Override
public void onFailed(AError aError) {
ALog.d(TAG, "onFailed() called with: aError = [" + aError + "]");
}
});
子設(shè)備離線
當子設(shè)備離線之后,網(wǎng)關(guān)需要通知物聯(lián)網(wǎng)平臺子設(shè)備離線,以避免物聯(lián)網(wǎng)平臺云端向子設(shè)備發(fā)送數(shù)據(jù)。子設(shè)備離線后,不可以進行子設(shè)備的發(fā)布、訂閱、取消訂閱等操作。
final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 產(chǎn)品型號(必填)
deviceInfo.deviceName = deviceName; // 設(shè)備標識(必填)
LinkKit.getInstance().getGateway().gatewaySubDeviceLogout(deviceinfo, new ISubDeviceActionListener() {
@Override
public void onSuccess() {
// 代理子設(shè)備下線成功
}
@Override
public void onFailed(AError aError) {
// 代理子設(shè)備下線失敗
}
});
監(jiān)聽子設(shè)備禁用
網(wǎng)關(guān)設(shè)備可以在物聯(lián)網(wǎng)平臺管理子設(shè)備,例如禁用子設(shè)備、啟用子設(shè)備、刪除和子設(shè)備的拓撲關(guān)系。
目前服務端只支持禁用子設(shè)備的下行通知。服務端在禁用子設(shè)備時會對子設(shè)備進行下線處理,后續(xù)網(wǎng)關(guān)將不能代理子設(shè)備和物聯(lián)網(wǎng)平臺云端通信。
final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 產(chǎn)品型號(必填)
deviceInfo.deviceName = deviceName; // 設(shè)備標識(必填)
LinkKit.getInstance().getGateway().gatewaySetSubDeviceDisableListener(deviceinfo, new IConnectRrpcListener() {
@Override
public void onSubscribeSuccess(ARequest aRequest) {
// 訂閱成功
}
@Override
public void onSubscribeFailed(ARequest aRequest, AError aError) {
// 訂閱失敗
}
@Override
public void onReceived(ARequest aRequest, IConnectRrpcHandle iConnectRrpcHandle) {
// 子設(shè)備禁用通知
iConnectRrpcHandle.onRrpcResponse(null, null);
}
@Override
public void onResponseSuccess(ARequest aRequest) {
Log.d(TAG, "onResponseSuccess() called with: aRequest = [" + aRequest + "]");
}
@Override
public void onResponseFailed(ARequest aRequest, AError aError) {
Log.d(TAG, "onResponseFailed() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]");
}
});
代理子設(shè)備上下行通信
物模型通信
子設(shè)備物模型初始化
子設(shè)備物模型初始化必須在子設(shè)備添加到網(wǎng)關(guān),且子設(shè)備已經(jīng)登錄的情況下才可以調(diào)用。
重要由于接口調(diào)用都是異步的,子設(shè)備物模型初始化接口不能在子設(shè)備登錄的下一行調(diào)用,而是要放到子設(shè)備登錄成功的回調(diào)中調(diào)用。
DeviceInfo deviceInfo = new DeviceInfo(); deviceInfo.productKey = productKey; deviceInfo.deviceName = deviceName; // deviceInfo.deviceSecret = "xxxx"; Map<String, ValueWrapper> subDevInitState = new HashMap<>(); // subDevInitState.put(); //TODO 用戶根據(jù)實際情況設(shè)置 String tsl = null;// 用戶根據(jù)實際情況設(shè)置,默認為空直接從云端獲取最細的 TSL LinkKit.getInstance().getGateway().initSubDeviceThing(tsl, deviceInfo, subDevInitState, new IDMCallback<InitResult>() { @Override public void onSuccess(InitResult initResult) { // 物模型初始化成功之后可以做服務注冊上報等操作 } @Override public void onFailure(AError aError) { // 子設(shè)備初始化失敗 } });
子設(shè)備物模型使用接口的調(diào)用和直連設(shè)備的物模型接口調(diào)用一致,獲取
IThing
接口的實現(xiàn)代碼如下。// 獲取 IThing 實例 IThing thing = LinkKit.getInstance().getGateway().getSubDeviceThing(mBaseInfo).first; // thing 有可能為空,如子設(shè)備未登錄、物模型未初始化、子設(shè)備未添加到網(wǎng)關(guān)、子設(shè)備處于離線狀態(tài)等。 // error 信息在 LinkKit.getInstance().getGateway().getSubDeviceThing(mBaseInfo).second 返回 // 參考示例,注意判空 thing.thingPropertyPost(reportData, new IPublishResourceListener() { @Override public void onSuccess(String s, Object o) { // 設(shè)備上報狀態(tài)成功 } @Override public void onError(String s, AError aError) { // 設(shè)備上報狀態(tài)失敗 } });
子設(shè)備物模型銷毀反初始化物模型。
后續(xù)如果需要重新使用,需要重新走登錄、物模型初始化流程。
LinkKit.getInstance().getGateway().uninitSubDeviceThing(mBaseInfo);
基礎(chǔ)通信
使用網(wǎng)關(guān)的通道執(zhí)行子設(shè)備的數(shù)據(jù)上下行。
final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 產(chǎn)品型號(必填)
deviceInfo.deviceName = deviceName; // 設(shè)備標識(必填)
String topic = xxx;
String publishData = xxx;
// 訂閱
LinkKit.getInstance().getGateway().gatewaySubDeviceSubscribe(topic, deviceinfo, new ISubDeviceActionListener() {
@Override
public void onSuccess() {
// 代理子設(shè)備訂閱成功
}
@Override
public void onFailed(AError aError) {
// 代理子設(shè)備訂閱失敗
}
});
//發(fā)布
LinkKit.getInstance().getGateway().gatewaySubDevicePublish(topic, publishData, deviceinfo, new ISubDeviceActionListener() {
@Override
public void onSuccess() {
// 代理子設(shè)備發(fā)布成功
}
@Override
public void onFailed(AError aError) {
// 代理子設(shè)備發(fā)布失敗
}
});
// 取消訂閱
LinkKit.getInstance().getGateway().gatewaySubDeviceUnsubscribe(topic, deviceinfo, new ISubDeviceActionListener() {
@Override
public void onSuccess() {
// 代理子設(shè)備取消訂閱成功
}
@Override
public void onFailed(AError aError) {
// 代理子設(shè)備取消訂閱事變
}
});