认证与连接

设备接入物联网平台之前,需通过身份认证。本文介绍如何将Android Link SDK进行初始化,实现将设备接入物联网平台。

前提条件

背景信息

Android Link SDK支持设备密钥和ID²认证进行设备身份认证。

  • 设备密钥:

    认证方式

    注册方式

    说明

    一机一密

    不涉及

    每台设备烧录自己的设备证书(ProductKey、DeviceName和DeviceSecret)。

    一型一密

    预注册

    • 同一产品下设备烧录相同产品证书(ProductKey和ProductSecret)。

    • 产品需开启动态注册功能。

    • 设备通过动态注册获取DeviceSecret。

    免预注册

    • 同一产品下设备烧录相同产品证书(ProductKey和ProductSecret)。

    • 产品需开启动态注册功能。

    • 设备通过动态注册获取ClientID与DeviceToken的组合。

    说明

    一型一密预注册和免预注册的区别,请参见预注册和免预注册的区别

  • ID²认证:ID²是一种物联网设备的可信身份标识,具备不可篡改、不可伪造等安全属性。

说明

具体代码实现,请参见Demo中的InitManager.java

一机一密

一机一密的设备认证方式的示例代码如下:

AppLog.setLevel(ALog.LEVEL_DEBUG);

final LinkKitInitParams params = new LinkKitInitParams();

String productKey = "${YourProductKey}";
String deviceName = "${YourDeviceName}";
String deviceSecret = "${YourDeviceSecret}";
String productSecret = "";

//Step1: 构造设备认证信息
DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.productKey = productKey;  // 产品类型
deviceInfo.deviceName = deviceName;  // 设备名称
deviceInfo.deviceSecret = deviceSecret;  // 设备密钥
deviceInfo.productSecret = productSecret;  // 产品密钥
params.deviceInfo = deviceInfo;

//Step2: 全局默认域名
IoTApiClientConfig userData = new IoTApiClientConfig();
params.connectConfig = userData;

//Step3: 物模型缓存
Map<String, ValueWrapper> propertyValues = new HashMap<>();
/**
 * 物模型的数据会缓存到该字段中,不可删除或者设置为空,否则功能会异常
 * 用户调用物模型上报接口之后,物模型会有相关数据缓存。
 */
params.propertyValues = propertyValues;

//Step4: mqtt设置
/**
 * Mqtt 相关参数设置,包括接入点等信息,具体参见deviceinfo文件说明
 * 域名、产品密钥、认证安全模式等;
 */
IoTMqttClientConfig clientConfig = new IoTMqttClientConfig();
clientConfig.receiveOfflineMsg = false;//cleanSession=1 不接受离线消息
//mqtt接入点信息
clientConfig.channelHost = "${YourMqttHostUrl}:8883";
params.mqttClientConfig = clientConfig;
//如果灭屏情况下经常出现设备离线,请参考下述的"设置自定义心跳和解决灭屏情况下的心跳不准问题"一节

//Step5: 高阶功能配置,除物模型外,其余默认均为关闭状态
IoTDMConfig ioTDMConfig = new IoTDMConfig();
// 默认开启物模型功能,物模型初始化(包含请求云端物模型)完成后才返回onInitDone
ioTDMConfig.enableThingModel = true;
// 默认不开启网关功能,开启之后,初始化的时候会初始化网关模块,获取云端网关子设备列表
ioTDMConfig.enableGateway = false;
// 默认不开启,是否开启日志推送功能
ioTDMConfig.enableLogPush = false;
params.ioTDMConfig = ioTDMConfig;

//Step6: 下行消息处理回调设置
LinkKit.getInstance().registerOnPushListener(new IConnectNotifyListener() {
    @Override
    public void onNotify(String s, String s1, AMessage aMessage) {
        //TODO: 处理下行消息的回调,请参考文档
    }

    @Override
    public boolean shouldHandle(String s, String s1) {
        return true; //TODO 根据实际情况设置,请参考文档
    }

    @Override
    public void onConnectStateChange(String connectId, ConnectState connectState) {
        // 对应连接类型的连接状态变化回调,具体连接状态参考SDK ConnectState
        AppLog.d(TAG, "onConnectStateChange() called with: connectId = [" + connectId + "], connectState = [" + connectState + "]");
    
        //首次连云可能失败。对于首次连云失败,SDK会报出ConnectState.CONNECTFAIL这种状态。对于这种场景,用户可以尝试若干次后退出,也可以一直重试直到连云成功
        //TODO: 以下是首次建连时用户主动重试的一个参考实现,用户可以打开下面注释使能下述代码
//      if(connectState == ConnectState.CONNECTFAIL){
//          try{
//              Thread.sleep(5000);
//              PersistentNet.getInstance().reconnect();
//          }catch (Exception e){
//              AppLog.d(TAG, "exception is " + e);
//          };
//          AppLog.d(TAG, "onConnectStateChange() try to reconnect when connect failed");
//      }
    
        //SDK连云成功后,后续如果网络波动导致连接断开时,SDK会抛出ConnectState.DISCONNECTED这种状态。在这种情况下,SDK会自动尝试重连,重试的间隔是1s、2s、4s、8s...128s...128s,到了最大间隔128s后,会一直以128s为间隔重连直到连云成功。
    }
});

//对于一型一密免预注册的设备, 设备连云时要用上deviceToken和clientId
//Step7: 一型一密免预注册设置,默认关闭
// MqttConfigure.deviceToken = DemoApplication.deviceToken;
// MqttConfigure.clientId = DemoApplication.clientId;

//Step8: H2文件上传设置
/**
 * 如果要用到HTTP2文件上传, 需要用户设置域名.默认关闭
 */
// IoTH2Config ioTH2Config = new IoTH2Config();
// ioTH2Config.clientId = "client-id";
// ioTH2Config.endPoint = "https://" + productKey + ioTH2Config.endPoint;// 线上环境
// params.iotH2InitParams = ioTH2Config;

/**
 * 设备初始化建联
 * onError 初始化建联失败,如果因网络问题导致初始化失败,需要用户重试初始化
 * onInitDone 初始化成功
 */
LinkKit.getInstance().init(getAppContext(), params, new ILinkKitConnectListener() {
    @Override
    public void onError(AError error) {
        ALog.d(TAG, "onError() called with: error = [" + (error) + "]");
    }

    @Override
    public void onInitDone(Object data) {
        ALog.d(TAG, "onInitDone() called with: data = [" + data + "]");
        //TODO 开始用户自己的业务
    }
});

一型一密

一型一密又称动态注册,用于向物联网平台获取设备的密钥,具体分为免预注册和预注册两种方式。使用该功能前,需要确保:

  • 已在物联网平台创建产品已开启动态注册开关。

  • Demo的deviceinfo文件中的deviceSecret的值为空,productSecret不为空。

  • 请确保已执行下面示例代码中的step 1、step 2、step 3。

  • 动态注册成功或失败之后,需要断开当前的动态注册长连接,具体参考step 4。

  • 为了您的设备安全,一型一密的认证方式获取到设备密钥后,请将其持久固化至设备,若需连接至物联网平台,请参考上述一机一密流程。

免预注册和预注册区别如下:

区别

预注册

免预注册

通信协议

MQTT、HTTPS

MQTT

地域支持

  • 基于MQTT的动态注册,物联网平台支持的所有地域。

  • 基于HTTPS的动态注册,仅限于上海区域,不推荐使用,不在本示例代码介绍范围之内。

华东2(上海)、华北2(北京)

返回的设备密钥

DeviceSecret具体使用方式,请参考上述一机一密示例中的Step1。

设备的ClientID和DeviceToken,请将其持久固化至设备,以便连云等其他功能使用。具体使用方式,请参考上述一机一密示例中的Step7。

添加设备

需要在物联网平台预注册设备DeviceName。

不需要在物联网平台预注册设备DeviceName。

使用次数限制

  • 同一组设备证书只能用于激活一个物理设备。若DeviceName名下已激活物理设备A,但物理设备B需要使用该DeviceName,则您可以在物联网平台上删除设备A,使设备A的DeviceSecret作废,再使用原DeviceName重新添加设备,激活物理设备B。

  • 若设备因丢失DeviceSecret等原因需要重新激活,需您调用ResetThing接口,重置设备状态为未激活,然后将设备重新联网激活。此时,物联网平台下发的DeviceSecret不变。

物联网平台允许最多5个物理设备使用同一组ProductKey、ProductSecret、DeviceName进行激活,并为不同物理设备颁发不同的ClientID、DeviceToken。

说明

具体代码实现,请参见Demo中的DemoApplication.java

示例代码如下:

String productKey = "${YourProductKey}";
String deviceName = "${YourDeviceName}";
String deviceSecret = "${YourDeviceSecret}";
String productSecret = "";
MqttInitParams initParams = new MqttInitParams(productKey, productSecret, deviceName, deviceSecret, MqttConfigure.MQTT_SECURE_MODE_TLS);

//动态注册step1: 确定一型一密的类型(免预注册, 还是非免预注册)
//case 1: 如果registerType里面填写了regnwl, 表明设备的一型一密方式为免预注册(即无需创建设备)
//case 2: 如果这个字段为空, 或填写"register", 则表示为需要预注册的一型一密(需要实现创建设备)
initParams.registerType = "";

//动态注册step2: 设置动态注册的注册接入点域名
MqttConfigure.mqttHost = "${YourMqttHostUrl}:8883";;

//动态注册step3: 对于企业实例, 或者2021年07月30日之后(含当日)开通的物联网平台服务下公共实例
//对于2021年07月30日之前(不含当日)开通的物联网平台服务下公共实例,该字段为空字符串""
MqttConfigure.registerInstanceId = "${YourInstanceId}";

final Object lock = new Object();
LinkKit.getInstance().deviceDynamicRegister(this, initParams, new IOnCallListener() {
    @Override
    public void onSuccess(com.aliyun.alink.linksdk.channel.core.base.ARequest request, com.aliyun.alink.linksdk.channel.core.base.AResponse response) {
        ALog.i(TAG, "onSuccess() called with: request = [" + request + "], response = [" + response + "]");
        // response.data is byte array
        try {
            String responseData = new String((byte[]) response.data);
            JSONObject jsonObject = JSONObject.parseObject(responseData);
            String pk = jsonObject.getString("productKey");
            String dn = jsonObject.getString("deviceName");
            // 一型一密预注册返回
            String deviceSecret = jsonObject.getString("deviceSecret");

            // 一型一密免预注册返回
            String clientId = jsonObject.getString("clientId");
            String deviceToken = jsonObject.getString("deviceToken");

            //TODO: 请用户保存用户密钥,不要在此做连云的操作,要等step 4执行完成后再做连云的操作(例如在其onSuccess分支中进行连云)

            //让等待的api继续执行
            synchronized (lock){
                lock.notify();
            }

        } catch (Exception e) {
        }

    }

    @Override
    public void onFailed(com.aliyun.alink.linksdk.channel.core.base.ARequest request, com.aliyun.alink.linksdk.channel.core.base.AError error) {
        ALog.e(TAG, "onFailed() called with: request = [" + request + "], error = [" + error + "]");
        //让等待的api继续执行
        synchronized (lock){
            lock.notify();
        }
    }

    @Override
    public boolean needUISafety() {
        return false;
    }
});

try{
    //等待下行报文,一般1s内就有回复
    synchronized (lock){
        lock.wait(3000);
    }
    
    //step 4: 退出动态注册
    //不要在LinkKit.getInstance().deviceDynamicRegister回调中执行下述函数,否则会报错
    LinkKit.getInstance().stopDeviceDynamicRegister(10 * 1000, null, new IMqttActionListener() {
        @Override
        public void onSuccess(IMqttToken iMqttToken) {
            ALog.d(TAG, "onSuccess() called with: iMqttToken = [" + iMqttToken + "]");
            // TODO: 在此处参考一机一密进行连云和初始化
        }

        @Override
        public void onFailure(IMqttToken iMqttToken, Throwable throwable) {
            ALog.w(TAG, "onFailure() called with: iMqttToken = [" + iMqttToken + "], throwable = [" + throwable + "]");
        }
    });
}catch (Exception e){

};

ID²认证

。/**
 * 在物联网平台创建使用iTLS认证方式的设备,采用该方式初始化
 * 产品需要ID²授权
 */
IoTMqttClientConfig clientConfig = new IoTMqttClientConfig(productKey, deviceName, deviceSecret);
clientConfig.channelHost = productKey + ".itls.cn-shanghai.aliyuncs.com:1883";
clientConfig.productSecret = productSecret;
clientConfig.secureMode = 8;
linkKitInitParams.mqttClientConfig = clientConfig;
//对于企业版实例, 或2021年07月30日之后(含当日)开通的物联网平台服务下公共实例,通过如下方式指定实例ID,其中${实例id}替换为具体ID值,格式为iot-*******
MqttConfigure.extraMqttClientIdItems=",instanceId=" + "${实例id}";   

示例代码中的参数clientConfig.channelHost为设备的接入域名。

更多设置

您可以设置以下参数,实现设备接入相关的更多设置。

  • MQTT连接:

    配置项

    说明

    相关代码

    保活时间

    设置设备的保活时间。通过该设置实现设备与物联网平台保持长连接。

    说明
    • 当前SDK默认保活时间为65秒。

    • 您可设置的心跳周期在30 ~ 1200秒。

    // interval 单位秒
    MqttConfigure.setKeepAliveInterval(int interval);

    QoS等级

    设置QoS(Quality of Service)等级,即物联网平台与设备之间保证交付信息的协议。仅支持:

    • 0:最多一次。

    • 1:最少一次。

    MqttPublishRequest request = new MqttPublishRequest();
    // 支持 0 和 1, 默认0
    request.qos = 0;
    request.isRPC = false;
    request.topic = topic.replace("request", "response");
    String resId = topic.substring(topic.indexOf("rrpc/request/")+13);
    request.msgId = resId;
    // TODO 用户根据实际情况填写
    request.payloadObj = "{\"id\":\"" + resId + "\", \"code\":\"200\"" + ",\"data\":{} }";

    离线消息

    通过cleanSession,设置是否接收离线消息。

    IoTMqttClientConfig clientConfig = new IoTMqttClientConfig();
    // 对应 receiveOfflineMsg = !cleanSession, 默认不接受离线消息 
    clientConfig.receiveOfflineMsg = true;
     

    客户端ID

    通过clientId设置客户端ID。clientId的详细说明,请参见clientId说明

    MqttConfigure.clientId = "abcdef******";

    重连机制

    通过automaticReconnect参数的值,设置是否重连。

    • true:重连。

    • false:不重连。

    参数设置:

    MqttConfigure.automaticReconnect = true;
  • SDK反初始化:

    如果需要注销初始化,调用如下反初始化接口。该接口为同步接口。

    说明

    SDK进行初始化时,需确保最近一次的反初始化已执行完成。否则,会导致重复初始化失败。

    // 取消注册 notifyListener,notifyListener对象需和注册的时候是同一个对象
    LinkKit.getInstance().unRegisterOnPushListener(notifyListener);
    LinkKit.getInstance().deinit();
  • 连接状态与下行消息监听:

    如果需要监听设备的上下线信息,物联网平台下发的所有数据,可以设置以下监听器。

    IConnectNotifyListener notifyListener = new IConnectNotifyListener() {
        @Override
        public void onNotify(String connectId, String topic, AMessage aMessage) {
            // 下行数据回调
            // connectId连接类型Topic下行Topic; aMessage下行数据
            // 数据解析如下:
            //String pushData = new String((byte[]) aMessage.data);
            // pushData 示例  {"method":"thing.service.test_service","id":"123374967","params":{"vv":60},"version":"1.0.0"}
            // method 服务类型; params 下推数据内容    
       }
        @Override
        public boolean shouldHandle(String connectId, String topic) {
            // 选择是否不处理某个Topic的下行数据
            // 如果不处理某个Topic,则onNotify不会收到对应Topic的下行数据
            return true; //TODO 根据实际情况设置
        }
        @Override
        public void onConnectStateChange(String connectId, ConnectState connectState) {
            // 对应连接类型的连接状态变化回调,具体连接状态参考SDK ConnectState
            AppLog.d(TAG, "onConnectStateChange() called with: connectId = [" + connectId + "], connectState = [" + connectState + "]");
        
            //首次连云可能失败。对于首次连云失败,SDK会报出ConnectState.CONNECTFAIL这种状态。对于这种场景,用户可以尝试若干次后退出,也可以一直重试直到连云成功
            //TODO: 以下是首次建连时用户主动重试的一个参考实现,用户可以打开下面注释使能下述代码
    //      if(connectState == ConnectState.CONNECTFAIL){
    //          try{
    //              Thread.sleep(5000);
    //              PersistentNet.getInstance().reconnect();
    //          }catch (Exception e){
    //              AppLog.d(TAG, "exception is " + e);
    //          };
    //          AppLog.d(TAG, "onConnectStateChange() try to reconnect when connect failed");
    //      }
        
            //SDK连云成功后,后续如果网络波动导致连接断开时,SDK会抛出ConnectState.DISCONNECTED这种状态。在这种情况下,SDK会自动尝试重连,重试的间隔是1s、2s、4s、8s...128s...128s,到了最大间隔128s后,会一直以128s为间隔重连直到连云成功。
        }
    }
    // 注册下行监听,包括长连接的状态和下行的数据
    LinkKit.getInstance().registerOnPushListener(notifyListener);
    // 取消下行监听,需要确保和注册的是同一个对象
    // LinkKit.getInstance().unRegisterOnPushListener(notifyListener);
                
    说明

    下行数据的回调onNotify,默认是在UI线程透出。自lp-iot-linkkit的1.7.3版本起,您可以通过PersistentConnect.mNotifyReceivedMsgOnMainThread = false;选择将下行消息通过非UI线程透出。对于下行消息密集,或者UI线程业务繁忙的场景,建议将该配置项设置为false

  • 日志开关:

    打开SDK内部日志输出开关:

    PersistentNet.getInstance().openLog(true);
    ALog.setLevel(ALog.LEVEL_DEBUG);
       

    安卓SDK提供了一个全量的拦截器,支持您重写拦截器的log函数,实现自定义的日志处理。例如:将日志通过持久化保存到特定文件中。

    日志输出示例代码:

            ALog.setLogDispatcher(new ILogDispatcher() {
                @Override
                public void log(int level, String prefix, String msg) {
                    switch (level){
                        case LEVEL_DEBUG:
                            System.out.println("debug:"+ prefix + msg);
                            break;
                        case LEVEL_INFO:
                            System.out.println("info:" + prefix + msg);
                            break;
                        case LEVEL_ERROR:
                            System.out.println("error:" + prefix + msg);
                            break;
                        case LEVEL_WARNING:
                            System.out.println("warnings:" + prefix + msg);
                            break;
                        default:
                            System.out.println("other:" + prefix + msg);
                    }
                }
            });
  • 获取SDK的版本号:

    ALog.i(TAG, "sdk version = " + LinkKit.getInstance().getSDKVersion());

安卓设备快速重连

安卓SDK断开后,快速重连的默认时间为65秒。部分场景下,您需要在断开后快速重连。请参考如下代码:

LinkKit.getInstance().reconnect();

设置自定义心跳和解决电池供电及灭屏情况下的心跳不准问题

  • 通过接口MqttPingSender,可实现自定义心跳时间。更多信息,请参见TimerPingSender。通过TimerPingSender,您可以设置下一次发送心跳的时间点,以及停止发送心跳。

    说明

    发送心跳的时间点不要超过保活时间。

    示例代码如下:

    // 其中PrivateMqttPingSender实现了MqttPingSender接口
    MqttConfigure.pingSender = new PrivateMqttPingSender();
  • 部分安卓6以及更高版本的电池供电的安卓设备在灭屏情况下,会因心跳发送不及时,导致设备离线。此时,您可参考接口AlarmMqttPingSender实现自定义心跳时间。

    重要
    • 该方式会定期唤醒安卓系统,会产生额外的功耗开销。

    • 对于安卓API level >= 31的设备:

      • 需要在AndroidManifest.xml文件中添加权限:

        android.permission.SCHEDULE_EXACT_ALARM

      • 在第67行调用API PendingIntent.getBroadcast时,flag字段(最后一个参数)需要根据实际情况考虑设置为FLAG_IMMUTABLE或者FLAG_MUTABLE

    • 在电池供电同时灭屏的情况下,安卓6以及更高版本的安卓系统的网络可能会由于Doze机制自动断开(例如系统日志中会出现Netd : Destroyed 3 sockets for UidRanges),这种情况下,即使定时器准确心跳报文还是无法正常发出,请联系您的安卓系统的供应商解决该问题。

Demo使用说明

  • 如果设备初始化失败,需再次对SDK进行初始化。当设备由于网络等原因导致与物联网平台断开连接时,SDK会自动尝试与物联网平台建立连接。

  • 修改Android Link SDK Demo./app/src/main/res/raw/deviceinfo文件的参数,实现设备的快速接入。表格中提示”需要填写“的参数进行填写,为否的参数保持默认设置不需修改。

  • 参数

    说明

    一机一密

    一型一密(预注册)

    一型一密(免预注册)

    productKey

    物联网平台为产品颁发的标识符。

    需要填写

    需要填写

    需要填写

    deviceName

    设备在产品内的标识符。

    需要填写

    需要填写

    需要填写

    productSecret

    产品密钥。

    需要填写

    需要填写

    deviceSecret

    设备密钥。

    需要填写

    registerType

    一型一密的方式,预注册或免预注册。

    需要填写,内容为字符串regnwl

    instanceId

    实例ID,对企业实例和新版本公共实例适用,具体请参见实例概述

    需要填写

    需要填写

    mqttHost

    MQTT接入域名。

    需要填写(上海区域除外),需要修改接入点的域名和端口号。

    • 使用新版公共实例、企业实例接入的用户,控制台的实例详情中带有端口号信息,可直接拷贝使用。格式如下: {instanceid}.mqtt.iothub.aliyuncs.com:8883

    • 使用旧版实例的用户,控制台的实例详情中没有端口号信息, 请拷贝后自行添加端口号信息。实例接入参考格式如下:

      {YourProductKey}.iot-as-mqtt.{region}.aliyuncs.com:8883具体请参见查看实例终端节点

常见问题

Android Link SDK相关问题