如果当前设备是一个网关,且该网关下的子设备需接入云端,此时需要使用子设备管理功能。 网关子设备管理提供了子设备动态注册、获取云端网关下子设备列表、添加子设备、删除子设备、子设备上线、子设备下线、监听子设备禁用和删除、代理子设备上下行的能力。 网关本身是一个直连设备可直接使用上述介绍的所有能力。网关和子设备之间的连接、数据通信需要用户处理。 说明 网关子设备管理相关接口参见设备 IGateway 设备每次初始建联的时候都需要调用添加、登录接口。

网关开发过程说明

  • 厂商在物联网平台定义网关产品(基础版或者高级版),设置“节点类型”为“网关”,设置网关的身份认证模式,并根据网关功能定义topic或者定义物模型,并参照前面的章节中的说明对网关自身的功能进行开发;

  • 实现子设备的管理功能

    1. 实现子设备的发现与连接功能,该部分功能由厂商自行实现,阿里并未提供网关如何发现以及如何将子设备连接到网关的代码实现

    2. 实现子设备三元组的获取方式,下面的内容有介绍几种获取子设备三元组的方式供厂商参考

    3. 当网关发现并将一个子设备连接到网关后,如果需要该子设备能够通过物联网平台进行远程管理,需要调用SDK的添加子设备接口将其告知物联网平台,添加之前需要先获得该子设备的三元组信息;然后调用SDK提供的子设备上线接口通知物联网平台,因为如果一个子设备处于离线状态,如果远程对子设备进行控制,物联网平台将直接返回失败、而不是将命令发送给网关之后等待错误提示或者超时提示

    4. 当网关告知物联网平台一个子设备上线之后,需要将子设备的状态信息上报云端以保证子设备在云端的状态与当前子设备的状态一致。特别是使用物模型定义子设备功能时,子设备上线需要将属性的最新数值通知云端;

    5. 当网关的一个已添加到物联网平台的子设备离线时,网关需要调用子设备离线接口告知物联网平台这个子设备已离线;

    6. 当一个在线子设备的属性发生变化时,也需要实时告知物联网平台

    7. 当网关离线并再次上线时(比如网络连接断开,或者网关重启),网关需要对所有已添加到云端的子设备再次调用添加子设备接口,再次调用子设备上线接口,如果网关不知道子设备的属性与网关离线前上报到云端的是否一致,那么网关需要将子设备的最新属性再次上报云端。

    8. 当网关接收到来自物联网平台对子设备的控制消息时,网关如何将该消息转换成子设备识别的格式并发送给子设备,由网关厂商进行实现

子设备开发过程

  • 设备厂商在物联网平台定义子设备产品(基础版或者高级版),设置“节点类型”为“设备”,设置产品的身份认证模式

  • 阿里并不在子设备上提供任何SDK,因此网关如何发现子设备、如何连接子设备、网关如何发现子设备上线或者离线、网关如何将来自物联网平台的命令发送给子设备,均由网关厂商与子设备厂商定义协议并实现

子设备三元组的获取方式

子设备是通过网关到阿里云物联网进行注册的,注册时也需要使用到子设备的三元组进行设备验证。下面是网关获取子设备三元组的几种方式,网关厂家根据自己的实际情况进行选用:

  • 网关从子设备获取子设备三元组

由网关与子设备之间定义一套协议,当网关发现与连接子设备之后,获取到子设备的三元组。阿里云并不提供参考协议实现,该协议由网关厂商与子设备厂商自行定义与实现;

  • 网关预置子设备的三元组

如果网关设备预先可以得知自己需要连接的子设备,并且网关提供了某种配置方式输入子设备的三元组信息,那么可以通过这种方式获取子设备的三元组。同样,该功能由网关厂商实现。

  • 网关通过动态注册获取子设备三元组

网关可以通过某种协议发现与连接子设备,并获取到子设备的型号(model)以及唯一标识(比如SN、MAC地址),但是并不知道子设备的DeviceSecret。由于子设备也需要在阿里云物联网平台进行产品定义(云端会为子设备生成ProductKey),网关可以建立子设备型号(model)到阿里云物联网平台ProductKey的映射(网关厂家在网关上实现该映射),并将设备的唯一标识作为阿里云物联网平台的DeviceName,然后通过阿里云物联网平台提供的动态注册功能从云端获取子设备的DeviceSecret,从而得到完整的子设备的三元组信息。

子设备动态注册

使用PK&DN动态注册

需要在云端开启允许动态注册功能,可以同时注册多个,用于获取子设备三元组。子设备添加到网关之前需要先进行动态注册获取子设备三元组信息,如果本地已有子设备三元组信息,可以跳过这一步。如果子设备已经被绑定到其他网关设备,动态注册不会返回该子设备的三元组信息。

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 {
            // 子设备动态注册成功
            ResponseModel<List<DeviceInfo>> response = JSONObject.parseObject(aResponse.data.toString(), new TypeReference<ResponseModel<List<DeviceInfo>>>() {
            }.getType());
            // 根据 response 的数据判断是否成功 code=200
            //TODO 保存子设备的三元组信息
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onFailure(ARequest aRequest, AError aError) {
        // 子设备动态注册失败
    }
});
			

使用PK&DN&PS动态注册

需要注意的是这种动态注册方式涉及到子设备的pk需要预先获取,安全性会低于第一种动态注册方式。如果当前已经有子设备的productKey、deviceName、productSecret信息,并且需要进行抢占式动态注册,则可以使用 这种方式进行子设备动态注册。抢占式动态注册,即被其它网关设备绑定的子设备三元组也会返回。推荐通过云端远程配置下发(COTA)下发子设备的PK、DN、PS信息到网关设备,然后再进行抢占式动态注册。应用场景:需要将子设备从A网关绑定到B网关的场景,如根据信号强度切换网关。使用PK&DN实现的话需要子设备先在A网关登出,解除拓扑关系,然后与B建立拓扑关系。

// 该动态注册方案需要提前知道子设备的productSecret,安全性会比下面一种子设备动态注册低一点
// 这种动态注册方式可以考虑和COTA-远程配置下发配合使用,在云端下发子设备的pk、dn、ps,网关收到后
// 完成动态注册
// 使用于需要抢占绑定关系时使用,会返回被其他网关设备的子设备
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("收到子设备动态结果");
            ResponseModel<Map<String, List<DeviceInfo>>> responseModel = JSONObject.parseObject(aResponse.data.toString(),
                                                                                                new TypeReference<ResponseModel<Map<String, List<DeviceInfo>>>>() {
                                                                                                }.getType());
            // TODO 保存子设备的三元组信息
            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"}
            // 动态注册成功列表
            List<DeviceInfo> successList = responseModel.data.get("successes");
            // 动态注册失败列表
            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 + "]");
    }
});
			

获取子设备列表

获取网关当前在云端已经有哪些子设备。

LinkKit.getInstance().getGateway().gatewayGetSubDevices(new IConnectSendListener() {
    @Override
    public void onResponse(ARequest aRequest, AResponse aResponse) {
        // 获取子设备列表结果
        try {
            ResponseModel<List<DeviceInfo>> response = JSONObject.parseObject(aResponse.data.toString(), new TypeReference<ResponseModel<List<DeviceInfo>>>() {
            }.getType());
            // TODO 根据实际应用场景处理
        } catch (Exception e) {
            e.printStackTrace();
        }
}

    @Override
    public void onFailure(ARequest aRequest, AError aError) {
        // 获取子设备列表失败
    }
});
			

添加子设备

子设备动态注册完成之后,可以通过该接口将子设备添加到网关下。

final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 三元组 产品型号(必填)
deviceInfo.deviceName = deviceName; // 三元组 设备标识 (必填)
LinkKit.getInstance().getGateway().gatewayAddSubDevice(deviceinfo, new ISubDeviceConnectListener() {
    @Override
    public String getSignMethod() {
        // 使用的签名方法
        return "hmacsha1";
    }

    @Override
    public String getSignValue() {
        // 获取签名,用户使用 deviceSecret 获得签名结果
        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) {
        // 添加结果
        if (isSuccess) {
            // 子设备添加成功,接下来可以做子设备上线的逻辑
            // subDevOnline(null);
        }
    }

    @Override
    public void onDataPush(String s, AMessage message) {
        // 收到子设备下行数据 topic=" + s  + ", data=" + message
        // 如禁用 删除 已经 设置、服务调用等 返回的数据message.data 是 byte[]
    }
});
			

删除子设备

删除网关下的子设备。

final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 三元组 产品型号(必填)
deviceInfo.deviceName = deviceName; // 三元组 设备标识 (必填)
LinkKit.getInstance().getGateway().gatewayDeleteSubDevice(deviceinfo, new ISubDeviceRemoveListener() {
    @Override
    public void onSuceess() {
        // 成功删除子设备 删除之前可先做下线操作
    }

    @Override
    public void onFailed(AError aError) {
        // 删除子设备失败
    }
});
			

子设备上线

调用子设备上线之前,请确保已完成子设备添加。网关发现子设备连上网关之后,需要告知云端子设备上线,子设备上线之后可以执行子设备的订阅、发布等操作。注意:由于接口调用都是异步的,子设备上线接口不能在子设备添加的下一行调用,而是要放到子设备添加成功的回调里面调用。

final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 三元组 产品型号(必填)
deviceInfo.deviceName = deviceName; // 三元组 设备标识 (必填)
LinkKit.getInstance().getGateway().gatewaySubDeviceLogin(deviceinfo, new ISubDeviceActionListener() {
    @Override
    public void onSuccess() {
        // 代理子设备上线成功
        // 上线之后可订阅 删除和禁用的下行通知
        // subDevDisable(null);
        // subDevDelete(null);
    }

    @Override
    public void onFailed(AError aError) {
        ALog.d(TAG, "onFailed() called with: aError = [" + aError + "]");
    }
});
			

子设备下线

当子设备离线之后,网关需要告知云端子设备离线,以避免云端向子设备发送数据。子设备下线之后不可以进行子设备的发布、订阅、取消订阅等操作。

final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 三元组 产品型号(必填)
deviceInfo.deviceName = deviceName; // 三元组 设备标识 (必填)
LinkKit.getInstance().getGateway().gatewaySubDeviceLogout(deviceinfo, new ISubDeviceActionListener() {
    @Override
    public void onSuccess() {
        // 代理子设备下线成功
    }

    @Override
    public void onFailed(AError aError) {
        // 代理子设备下线失败
    }
});
			

监听子设备禁用

网关设备可以在云端操作子设备,如禁用子设备、启用子设备、删除和子设备的拓扑关系。目前服务端只支持禁用子设备的下行通知。服务端在禁用子设备的时候会对子设备做下线处理,后续网关将不能代理子设备和云端做通信。

final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 三元组 产品型号(必填)
deviceInfo.deviceName = deviceName; // 三元组 设备标识 (必填)
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) {
        // 子设备禁用通知
        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 + "]");
    }
});
			

代理子设备物模型上下行

  • 子设备物模型初始化

子设备物模型初始化必须在子设备添加到网关下,且子设备已经登录的情况下才可以调用。注意:由于接口调用都是异步的,子设备物模型初始化接口不能在子设备登录的下一行调用,而是要放到子设备登录成功的回调里面调用。

DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey;
deviceInfo.deviceName = deviceName;
//        deviceInfo.deviceSecret = "xxxx";
Map<String, ValueWrapper> subDevInitState = new HashMap<>();
//        subDevInitState.put(); //TODO 用户根据实际情况设置
String tsl = null;// 用户根据实际情况设置,默认为空 直接从云端获取最细的 TSL
LinkKit.getInstance().getGateway().initSubDeviceThing(tsl, deviceInfo, subDevInitState, new IDMCallback<InitResult>() {
    @Override
    public void onSuccess(InitResult initResult) {
        // 物模型初始化成功之后 可以做服务注册  上报等操作
    }

    @Override
    public void onFailure(AError aError) {
        // 子设备初始化失败
    }
});
			
  • 子设备物模型使用接口使用和直连设备的物模型使用一致,获取 IThing 接口实现使用如下方式获取。
// 获取 IThing 实例
IThing thing = LinkKit.getInstance().getGateway().getSubDeviceThing(mBaseInfo).first;
// thing 有可能为空,如子设备未登录、物模型未初始化、子设备未添加到网关、子设备处于离线状态等。
// error 信息在 LinkKit.getInstance().getGateway().getSubDeviceThing(mBaseInfo).second 返回
// 参考示例 注意判空
thing.thingPropertyPost(reportData, new IPublishResourceListener() {
    @Override
    public void onSuccess(String s, Object o) {
        // 设备上报状态成功
    }

    @Override
    public void onError(String s, AError aError) {
        // 设备上报状态失败
    }
});
			
  • 子设备物模型销毁反初始化物模型,后续如果需要重新使用需要重新走登录、物模型初始化流程。
LinkKit.getInstance().getGateway().uninitSubDeviceThing(mBaseInfo);
			

代理子设备基础上下行

使用网关的通道执行子设备的数据上下行。

final DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey; // 三元组 产品型号(必填)
deviceInfo.deviceName = deviceName; // 三元组 设备标识 (必填)
String topic = xxx;
String publishData = xxx;
// 订阅
LinkKit.getInstance().getGateway().gatewaySubDeviceSubscribe(topic, deviceinfo, new ISubDeviceActionListener() {
    @Override
    public void onSuccess() {
        // 代理子设备订阅成功
    }

    @Override
    public void onFailed(AError aError) {
        // 代理子设备订阅失败
    }
});

//发布
LinkKit.getInstance().getGateway().gatewaySubDevicePublish(topic, publishData, deviceinfo, new ISubDeviceActionListener() {
    @Override
    public void onSuccess() {
        // 代理子设备发布成功
    }

    @Override
    public void onFailed(AError aError) {
        // 代理子设备发布失败
    }
});

// 取消订阅
LinkKit.getInstance().getGateway().gatewaySubDeviceUnsubscribe(topic, deviceinfo, new ISubDeviceActionListener() {
    @Override
    public void onSuccess() {
        // 代理子设备取消订阅成功
    }

    @Override
    public void onFailed(AError aError) {
        // 代理子设备取消订阅事变
    }
});
			

支持PermitJoin

如果网关是接入智能生活开放平台(又称飞燕平台),网关需要在收到来自云端的PermitJoin消息时,再连接子设备、以及添加子设备。目前的版本还不支持对PermitJoin的封装(将在下一个迭代中进行支持),可以通过下面的办法实现对PermitJoin消息的处理:

  • 网关需要去发现并连接指定型号的子设备,如果网关找不到指定型号的子设备,那么就无需添加子设备;添加子设备的接口见本章前面描述的“添加子设备

  • 如果网关收到PermitJoin之后,发现网关连接了多个指定型号的子设备,只向云端添加其中一个子设备即可

// 订阅并监听云端 permitJoin 下行
LinkKit.getInstance().getGateway().permitJoin(new IConnectRrpcListener() {
    @Override
    public void onSubscribeSuccess(ARequest aRequest) {
        ALog.d(TAG, "onSubscribeSuccess() called with: aRequest = [" + aRequest + "]");
        showToast("permitJoin subscribe success.");
    }

    @Override
    public void onSubscribeFailed(ARequest aRequest, AError aError) {
        ALog.e(TAG, "onSubscribeFailed() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]");
    }

    @Override
    public void onReceived(ARequest aRequest, IConnectRrpcHandle iConnectRrpcHandle) {
        ALog.d(TAG, "onReceived() called with: aRequest = [" + aRequest + "], iConnectRrpcHandle = [" + iConnectRrpcHandle + "]");
        showToast("接收到permitJoin指令");
        // TODO  user add sub device &
        if (aRequest instanceof MqttRrpcRequest){
            // 云端下行数据 拿到
            // ((MqttRrpcRequest) aRequest).payloadObj;
            // ResponseModel<Map<String, String>> responseModel = JSONObject.parseObject(((MqttRrpcRequest) aRequest).payloadObj, new TypeReference<ResponseModel<Map<String, String>>>(){}.getType());
            // 返回数据示例
            //{"id":"",    //消息id
//                 "version":"1.0",    //ALink协议的版本号
//                 "params":{
//                     "productKey":"xxx",   //子设备的型号,如果内容为空,表示网关允许任何子设备接入
//                     "time":60    //网关发现与添加子设备的窗口时间,int类型,单位为秒
//                  }
//             }
            // TODO 发现、添加、登录 子设备

            // 网关处理完子设备拓扑关系添加、登录后
            if (iConnectRrpcHandle != null) {
                AResponse response = new AResponse();
                // 回复示例 TODO edit by user
                // response.data 里的 id 字段需要与收到的 permitJoin 消息的 id 保持一致
                response.data = "{\"id\":\"xxx\", \"code\":\"200\"" + ",\"data\":{} }";
                iConnectRrpcHandle.onRrpcResponse(((MqttRrpcRequest) aRequest).replyTopic, response);
            }
        }
    }

    @Override
    public void onResponseSuccess(ARequest aRequest) {
        ALog.d(TAG, "onResponseSuccess() called with: aRequest = [" + aRequest + "]");
    }

    @Override
    public void onResponseFailed(ARequest aRequest, AError aError) {
        ALog.d(TAG, "onResponseFailed() called with: aRequest = [" + aRequest + "], aError = [" + aError + "]");
    }
});