网关与子设备

网关设备及其下子设备接入物联网平台时,需要使用子设备管理功能。 本文介绍如何配置和开发网关子设备接入物联网平台。

功能说明

网关与子设备管理功能提供了子设备动态注册、获取云端网关子设备列表、添加子设备、删除子设备、子设备上线、子设备离线、监听子设备禁用和删除、代理子设备上下行通信的能力。网关本身是一个直连设备可直接使用上述所有能力。网关和子设备之间的连接、数据通信需要用户处理。

开发说明

网关开发过程说明

  • 厂商在物联网平台定义网关产品时,设置节点类型网关设备,设置网关的身份认证方式,并根据网关功能定义Topic或物模型,并参照前面的章节中的说明对网关自身的功能进行开发。

  • 实现子设备的管理功能:

    1. 子设备的发现与连接功能。

      由厂商自行实现,阿里不提供网关如何发现以及如何将子设备连接到网关的代码实现。

    2. 子设备证书的获取方式。

      下文介绍的获取子设备证书的方式可供厂商参考。

    3. 物联网平台远程管理网关下子设备。

      当网关发现并接入子设备后,需要调用SDK的添加子设备接口通知物联网平台:先获得该子设备的证书信息,然后调用SDK提供的子设备上线接口通知物联网平台。

      说明

      如果一个子设备处于离线状态,物联网平台远程控制子设备时,会直接返回失败,不会将命令发送给网关再等待错误提示或者超时提示。

    4. 同步子设备运行状态到物联网平台。

      • 当网关通知物联网平台一个子设备上线后,需要将子设备的状态信息上报云端,以保证子设备在云端的状态与当前子设备的状态一致。

        说明

        如果使用物模型定义子设备功能时,子设备上线需要将属性的最新数值上报到物联网平台。

      • 当网关代理接入物联网平台的子设备离线时,网关需要调用子设备离线接口,通知物联网平台该子设备已离线。

      • 当在线子设备的属性发生变化时,也需要实时上报到物联网平台。

      • 当网关离线并再次上线时(例如网络连接断开或网关重启),网关需要对所有已添加到物联网平台的子设备,再次调用添加子设备接口和子设备上线接口。如果网关不知道子设备的属性与网关离线前上报到物联网平台的是否一致,则网关需要将子设备的最新属性再次上报到物联网平台。

    5. 子设备接收物联网平台下发的消息。

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

子设备开发说明

  • 设备厂商在物联网平台定义子设备产品,设置节点类型网关子设备,设置产品的身份认证方式。

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

网关的认证与连接

获取子设备证书

子设备是通过网关代理在阿里云物联网平台进行注册的,注册时需要使用到子设备的证书进行身份验证。网关获取子设备证书的方式如下,网关厂家可根据自己的实际情况进行选用:

  • 网关从子设备获取子设备证书。

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

  • 网关预置子设备的证书。

    如果网关设备可预先获取需要连接的子设备信息,并且网关提供了某种配置方式输入子设备的证书信息,则可以通过该方式获取子设备的证书。同样,该功能由网关厂商实现。

  • 网关通过动态注册获取子设备证书。

    网关可以通过某种协议发现与连接子设备,并获取到子设备的型号(model)以及唯一标识(例如SNMAC地址),但并不知道子设备的DeviceSecret时,可通过以下步骤获取:

    1. 子设备在阿里云物联网平台进行产品定义,云端会为子设备生成ProductKey

    2. 网关设备建立子设备型号(model)到阿里云物联网平台ProductKey的映射(网关厂家在网关上实现该映射),并将设备的唯一标识作为阿里云物联网平台的DeviceName

    3. 网关通过阿里云物联网平台提供的动态注册功能,从云端获取子设备的DeviceSecret,从而得到完整的子设备的证书信息。

使能网关模块

在网关初始化时,将enableGateway选项设置为true。具体配置,请参见认证与连接中一机一密ioTDMConfig.enableGateway配置。

// 默认不开启网关功能,开启之后,初始化的时候会初始化网关模块,获取云端网关子设备列表
ioTDMConfig.enableGateway = true;

子设备动态注册

使用ProductKey和DeviceName动态注册

子设备添加到网关之前需要先进行动态注册获取子设备证书信息,您需要在物联网平台开启动态注册功能。动态注册支持同时注册多个子设备,用于获取子设备证书。

说明

如果本地已有子设备证书信息,可以跳过此步骤。如果子设备已经被绑定到其他网关设备,动态注册不会返回该子设备的证书信息。

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) {
        // 子设备动态注册失败
    }
});
            

使用ProductKey、DeviceName、ProductSecret动态注册

如果当前已经有子设备的ProductKeyDeviceNameProductSecret信息,并且需要进行抢占式动态注册,则可以使用该方式进行子设备动态注册。抢占式动态注册是指被其它网关设备绑定的子设备证书也会返回。

说明
  • 该动态注册方式需要预先获取子设备的ProductSecret,安全性会低于第一种动态注册方式。

  • 推荐通过物联网平台远程配置下发(COTA)子设备的ProductKeyDeviceNameProductSecret信息到网关设备,然后再进行抢占式动态注册。

应用场景

需要将子设备从A网关绑定到B网关的场景。

代码实现

// 该动态注册方案需要提前知道子设备的productSecret,安全性会比下面一种子设备动态注册低一点
// 这种动态注册方式可以考虑和COTA-远程配置下发配合使用,在云端下发子设备的ProductKey、DeviceName、ProductSecret,网关收到后
// 完成动态注册
// 使用于需要抢占绑定关系时使用
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) {
        // 代理子设备取消订阅事变
    }
});