本文以TG7100C芯片为例,介绍基于生活物联网平台SDK(V1.6.6)中的smart_outlet应用示例,开发单孔Wi-Fi智能插座设备固件的流程。

背景信息

应用示例smart_outlet的功能介绍如下:

  • 支持云智能App(V3.5.5以上)与天猫精灵App(4.13.0以上)蓝牙辅助配网。
  • 支持通过云端、本地通信(目前仅云智能App支持)对设备进行控制的能力。
  • 支持通过生活物联网平台进行设备OTA的能力。
  • 支持恢复工厂设置。
  • 支持断电用户设置记忆。

TG7100C概述

TG7100C是天猫精灵推出的Wi-Fi蓝牙Combo芯片。TG7100C芯片相关文档和软件工具介绍,请参见TG7100C

说明

关于TG7100B芯片的驱动、产测、硬件设计、射频等使用问题,以及基于生活物联网平台蓝牙Mesh SDK的应用开发,例如产品配置、配网、连云、OTA等问题,您可以通过商务联系技术支持进行反馈。

固件编译

  1. 下载SDK。下载地址,请参见获取SDK
  2. 配置开发环境。详细介绍,请参见准备开发环境
  3. 编译代码。
    1. (可选)解压SDK压缩包。
      如果您通过Git命令的方式下载SDK,则无需解压。

      SDK根目录build.sh文件说明:根据硬件使用的模组型号和要编译的应用,可以修改文件中的如下参数。

      default_type="example"     //配置产品类型
      default_app="smart_outlet" //配置编译的应用名称
      default_board="tg7100cevb" //配置编译的模组型号
      default_region=MAINLAND    //配置设备的连云区域,配置为MAINLAND或SINGAPORE都可以,设备可以全球范围内激活
      default_env=ONLINE         //配置连云环境,默认设置为线上环境(ONLINE)
      default_debug=0            //配置debug等级,生产固件建议为0
      default_args=""            //配置其他编译参数
      //更多介绍,请参见README.md
    2. 将开发的业务代码存放到SDK相应的目录下,例如非计量插座标品代码在Products/example/smart_outlet 目录下。
    3. 执行以下命令,快速编译smart_outlet应用示例。
      ./build.sh example smart_outlet tg7100cevb MAINLAND ONLINE 0

      编译完成后,在out/smart_outlet@tg7100cevb/目录下会生成smart_outlet@tg7100cevb.bin文件。

    4. smart_outlet@tg7100cevb.bin文件烧录到真实设备中。
      tg7100cevb_ota.bin文件为OTA使用的固件。TG7100C编译结果

固件烧录与运行

  1. 在TG7100C开发板上烧录固件。详细操作,请参见TG7100C开发板用户手册
  2. 短路接通开发板的第4个引脚与第5个引脚,并按开发板的复位键。
    开发板设置图如下。开发板烧录模式
  3. 打开下载好的烧录工具目录中的TGFlashEnv.exe,单击Finish,进入烧录界面。
  4. Interface选择Uart,并单击Refresh按键。
  5. 设置串口参数,以及选择好对应的烧录文件。
    串口参数的配置如下图所示。
    • Partition TableBoot 2 BinMFG Bin选择烧录工具目录下对应的文件即可,文件名如图。
    • Firmware Bin选择编译出的固件。
    • Chip Erase可以根据是否要擦除整片Flash选择True或者False,如调试中要保留之前写入过的设备证书,可以选择False
    TG7100C烧录软件
  6. 单击Download按钮,并同时按下开发板上的复位键,开始烧录固件。
  7. 烧录完毕后,查看运行的日志。
    短路连接第3个引脚与第4个引脚(如下图所示),将串口工具波特率设置为2000000,并按下开发板复位键。开发板运行模式

    常用的cli指令如下。

    • reset:设备重置,清除设备配网信息。
    • free:查看内存使用情况。
    • linkkey:写入与查看证书。
    • mac:查看开发板Wi-Fi MAC地址。

smart_outlet应用代码结构介绍

smart_outlet应用示例中的文件结构如下。

├── Products
│   │   ├── example/smart_outlet
│   │   │   ├── app_entry.c
│   │   │   ├── app_entry.h
│   │   │   ├── combo_net.c
│   │   │   ├── device_state_manager.c
│   │   │   ├── device_state_manager.h
│   │   │   ├── factory.c
│   │   │   ├── factory.h
│   │   │   ├── makefile
│   │   │   ├── make.settings
│   │   │   ├── msg_process_center.c
│   │   │   ├── msg_process_center.h
│   │   │   ├── property_report.c
│   │   │   ├── property_report.h
│   │   │   ├── smart_outlet.h
│   │   │   ├── smart_outlet.json
│   │   │   ├── smart_outlet_main.c
│   │   │   ├── smart_outlet.mk
│   │   │   ├── vendor.c
│   │   │   └── vendor.h

详细的文件说明如下。

  • 厂家需要适配的文件(设备初始化等):vendor.cvendor.h
  • 应用程序主入口:app_entry.csmart_outlet_main.c
  • 配网和连云状态管理:device_state_manager.c
  • 设备控制指令处理:msg_process_center.c
  • 设备属性上报:property_report.c
  • 厂测模式:factory.c
  • 蓝牙辅助配网:combo_net.c

固件适配说明

标品固件移植适配对单路智能插座应用,只需要较小的修改,就可以完成产品固件的输出。根据产品的不同需求,涉及到的调整项介绍如下。

  • GPIO适配

    单路插座需要两个GPIO分别控制继电器开关、LED亮灭和一个GPIO读取按键状态。那么只需要修改vendor.c中定义,实例如下。

    ......
    #elif (defined (TG7100CEVB))
    #define LED_GPIO    1               // 控制LED亮灭
    #define RELAY_GPIO  5               // 控制继电器开关
    #define KEY_GPIO    3               // 读取按键状态
    ......

    产品开发时,可以根据具体的原理图设计配置对应的GPIO。

  • 状态LED显示适配
    • 设备状态定义在文件Products/example/smart_outlet/device_state_manager.h 中。
      typedef enum {
          RECONFIGED = 0,            //reconfig with netconfig exist
          UNCONFIGED,                //配网开始
          AWSS_NOT_START,            //配网超时
          GOT_AP_SSID,               //连接AP成功
          CONNECT_CLOUD_SUCCESS,     //连云成功
          CONNECT_CLOUD_FAILED,      //连云失败
          CONNECT_AP_FAILED,         //连接AP失败
          CONNECT_AP_FAILED_TIMEOUT, //连接AP超时
          APP_BIND_SUCCESS,          //APP绑定成功
          ...
          UNKNOW_STATE
      } eNetState;
    • 状态显示的处理代码在文件Products/example/smart_outlet/device_state_manager.c 中的indicate_net_state_task 函数中。可以根据产品的不同需求做调整。
      static void indicate_net_state_task(void *arg)
      {
          uint32_t nCount = 0;
          uint32_t duration = 0;
          int pre_state = UNKNOW_STATE;
          int cur_state = UNKNOW_STATE;
          int switch_stat = 0;
      
          while (1) {
              pre_state = cur_state;
              cur_state = get_net_state();
              switch (cur_state) {
                  case RECONFIGED:
                      ...
                      break;
                  case UNCONFIGED:
                      ...
                      break;
                  case AWSS_NOT_START:
                      ...
                      break;
                  case GOT_AP_SSID:
                  case CONNECT_CLOUD_FAILED:
                      ...
                      break;
                  case CONNECT_AP_FAILED_TIMEOUT:
                      ...
                      break;
                  case CONNECT_AP_FAILED:
                      ...
                      break;
                  case CONNECT_CLOUD_SUCCESS:
                      ...
                      break;
                  case APP_BIND_SUCCESS:        
                      ...
                      break;
                  ...
                  default:
                      break;
              }
              aos_msleep(100);
          }
      
          ...
      }
    • 当前代码中实现的默认LED显示如下。
      状态默认LED显示
      配网模式插座LED反复闪烁,亮0.8秒,灭0.8秒。
      恢复出厂设置插座LED反复闪烁,亮0.2秒,灭0.2秒。
      连接AP 超时/连接AP 认证失败(超时时间2分钟)插座LED反复闪烁的模式更改为,亮0.5秒、灭0.5秒,闪烁两分钟之后停止闪烁。停止闪烁之后,如果插座配电使能则LED灯点亮,否则LED灯灭掉。
      连接AP成功、尝试连云插座LED反复闪烁,亮0.8秒,灭0.8秒,然后开始尝试连接云端。
      连云失败连接云端失败后,需要再次尝试连接,其间LED的显示与“连接AP成功、尝试连云”模式一样。
      连云成功当设备连接云端成功,则停止LED闪烁,若插座配电打开则LED点亮,若插座配电未打开则LED灭掉。
  • 按键处理适配
    标品固件根据用户按下按键的时长,确定用户的行为,目前按键有三种用户行为处理。代码Products/example/smart_outlet/device_state_manager.c文件中的key_detect_event_task函数负责按键处理。如下定义了各种行为的时间,如果需要调整各个行为的按键时长,可以自行修改。
    #define AWSS_REBOOT_TIMEOUT (4 * 1000) //长按4s 进入网络配置模式,开始重新配网
    #define AWSS_RESET_TIMEOUT (6 * 1000) //长按6s 进入恢复出厂设置,(在设备已进入网络配置模式下)
    #define KEY_PRESSED_VALID_TIME_MIN 100
    #define KEY_PRESSED_VALID_TIME_MAX 500 //按键按下超过100ms,小于500ms,表示有按键按下
    #define KEY_DETECT_INTERVAL 50 //按键按下的检测时间间隔 50ms
    #define AWSS_REBOOT_CNT AWSS_REBOOT_TIMEOUT /KEY_DETECT_INTERVAL
    #define AWSS_RESET_CNT AWSS_RESET_TIMEOUT /KEY_DETECT_INTERVAL
    #define KEY_PRESSED_CNT KEY_PRESSED_VALID_TIME /KEY_DETECT_INTERVAL
    
    // 此函数处理插座按键检测
    void key_detect_event_task(void *arg)
    {
        int nCount = 0, awss_mode = 0;
        int timeout = (AWSS_REBOOT_CNT < AWSS_RESET_TIMEOUT)? AWSS_REBOOT_CNT : AWSS_RESET_TIMEOUT;
    
        while (1) {
            if (!product_get_key()) {
                nCount++;
                LOG("nCount :%d", nCount);
            } else {
                if (nCount >= KEY_PRESSED_CNT && nCount < timeout) {  // 按键控制
                    if (product_get_switch() == ON) {   // 按键控制插座关闭继电器
                        product_set_switch(OFF);
                        user_post_powerstate(OFF);
                    } else {                            // 按键控制插座打开继电器
                        product_set_switch(ON);
                        user_post_powerstate(ON);
                    }
                }
                if ((awss_flag == 0) && (nCount >= AWSS_REBOOT_CNT)) {
                    LOG("do awss reboot");              // 长按4s 进入网络配置模式,开始重新配网
                    do_awss_reboot();
                    break;
                } else if ((awss_flag == 1) && (nCount > AWSS_RESET_CNT)) {
                    LOG("do awss reset");               // 长按6s 进入恢复出厂设置
                    do_awss_reset();                    // 实际执行设备重置
                    break;
                }
                nCount = 0;
            }
            if ((awss_flag == 0) && (nCount >= AWSS_REBOOT_CNT && awss_mode == 0)) {
                set_net_state(RECONFIGED);              // 设置相应的设备状态
                awss_mode = 1;
            } else if ((awss_flag == 1) && (nCount > AWSS_RESET_CNT && awss_mode == 0)) {
                set_net_state(UNCONFIGED);              // 设置相应的设备状态
                awss_mode = 1;
            }
            aos_msleep(KEY_DETECT_INTERVAL);            // 检测按键间隔为50ms
        }
        aos_task_exit(0);
    }
    • 短按:如果按键按下时长长于100ms,小于500ms, 认为用户是进行按键开关。
    • 长按:如果用户按下时间超过4s,认为用户触发设备进入网络配置模式。如果用户确认设备已经进入网络配置模式,此时继续按键6s,设备会进入恢复出厂模式。

设备端通用功能说明

以下功能在smart_outlet应用示例中已有相关实现,仅对设备端的通用功能做一些补充介绍。

  • 事件回调
    smart_outlet_main.c文件中定义了系统的各种事件处理函数,在linkkit_main函数中注册了回调函数。
    int linkkit_main()
    {
        ...
        
        /* Register Callback */
        IOT_RegisterCallback(ITE_CONNECT_SUCC, user_connected_event_handler);
        IOT_RegisterCallback(ITE_DISCONNECTED, user_disconnected_event_handler);
        // IOT_RegisterCallback(ITE_RAWDATA_ARRIVED, user_down_raw_data_arrived_event_handler);
        IOT_RegisterCallback(ITE_SERVICE_REQUEST, user_service_request_event_handler);
        IOT_RegisterCallback(ITE_PROPERTY_SET, user_property_set_event_handler);
    #ifdef ALCS_ENABLED
        /*Only for local communication service(ALCS) */
        IOT_RegisterCallback(ITE_PROPERTY_GET, user_property_get_event_handler);
    #endif
        IOT_RegisterCallback(ITE_REPORT_REPLY, user_report_reply_event_handler);
        IOT_RegisterCallback(ITE_TRIGGER_EVENT_REPLY, user_trigger_event_reply_event_handler);
        IOT_RegisterCallback(ITE_INITIALIZE_COMPLETED, user_initialized);
        IOT_RegisterCallback(ITE_EVENT_NOTIFY, user_event_notify_handler);
        ...
    }
    事件事件触发条件说明
    ITE_CONNECT_SUCC与云端连接成功时
    ITE_DISCONNECTED与云端连接断开时
    ITE_RAWDATA_ARRIVEDSDK收到raw data数据时
    ITE_SERVICE_REQUESTSDK收到服务(同步/异步)调用请求时
    ITE_PROPERTY_SETSDK收到属性设置请求时
    ITE_PROPERTY_GETSDK收到属性获取的请求时
    ITE_REPORT_REPLYSDK收到上报消息的应答时
    ITE_TRIGGER_EVENT_REPLYSDK收到事件上报消息的应答时
    ITE_EVENT_NOTIFYSDK收到事件通知时
    ITE_INITIALIZE_COMPLETED设备初始化完成时
  • 属性上报
    产品的属性发生变化时,需要将变化后的数值上报到物联网平台。可以根据产品需求增加属性变化的检测以及上报逻辑。
    void user_post_property(property_report_msg_t * msg)
    {
        int res = 0;
        user_example_ctx_t *user_example_ctx = user_example_get_ctx();
        char *property_payload = NULL;
        cJSON *response_root = NULL, *item_csr = NULL;
    
        response_root = cJSON_CreateObject();
        if (response_root == NULL) {
            return;
        }
    
        if (msg->seq != NULL && strcmp(msg->seq, SPEC_SEQ)) {
            item_csr = cJSON_CreateObject();
            if (item_csr == NULL) {
                cJSON_Delete(response_root);
                return;
            }
            cJSON_AddStringToObject(item_csr, "seq", msg->seq);
            cJSON_AddItemToObject(response_root, "CommonServiceResponse", item_csr);
        }
    #ifdef TSL_FY_SUPPORT
        //兼容旧版本开关PowerSwitch属性
        cJSON_AddNumberToObject(response_root, "PowerSwitch", msg->powerswitch);
    #endif
        //处理新版本物模型开关powerstate属性
        cJSON_AddNumberToObject(response_root, "powerstate", msg->powerswitch);
        //处理新版物模型allPowerstate属性
        cJSON_AddNumberToObject(response_root, "allPowerstate", msg->all_powerstate);
        property_payload = cJSON_PrintUnformatted(response_root);
        cJSON_Delete(response_root);
    
        char *property_formated;
        uint32_t len;
        res = user_property_format(property_payload,strlen(property_payload),&property_formated,&len);
    #ifdef EN_COMBO_NET //对于Wi-Fi&BLE Combo设备可以同时通过蓝牙控制链路上报属性值。
        if (combo_ble_conn_state()) {
            if (0 == res) {
                combo_status_report(property_formated, strlen(property_formated));
                LOG_TRACE("Post Property Message ID: %d Payload %s", res, property_formated);
            } else {
                combo_status_report(property_payload, strlen(property_payload));
                LOG_TRACE("Post Property Message ID: %d Payload %s", res, property_payload);
            }
        }
    #endif
        if (0 == res) {
            if (msg->seq != NULL && strcmp(msg->seq, SPEC_SEQ)) {
                res = IOT_Linkkit_Report_Ext(user_example_ctx->master_devid, ITM_MSG_POST_PROPERTY,
                        (unsigned char *)property_formated, strlen(property_formated), msg->flag);
            } else {
                res = IOT_Linkkit_Report(user_example_ctx->master_devid, ITM_MSG_POST_PROPERTY,
                        (unsigned char *)property_formated, strlen(property_formated));
            }
            LOG_TRACE("Post Property Message ID: %d Payload %s", res, property_formated);
            example_free(property_formated);
        } else {
            if (msg->seq != NULL && strcmp(msg->seq, SPEC_SEQ)) {
                res = IOT_Linkkit_Report_Ext(user_example_ctx->master_devid, ITM_MSG_POST_PROPERTY,
                        (unsigned char *)property_payload, strlen(property_payload), msg->flag);
            } else {
                res = IOT_Linkkit_Report(user_example_ctx->master_devid, ITM_MSG_POST_PROPERTY,
                        (unsigned char *)property_payload, strlen(property_payload));
            }
            LOG_TRACE("Post Property Message ID: %d Payload %s", res, property_payload);
        }
        example_free(property_payload);
    }
  • 属性设置
    smart_outlet按对ITE_PROPERTY_SET注册的回调函数,在回调函数user_property_set_event_handler中获取云端设置的属性值,并原样将收到的数据发回给云端,这样可以更新在云端的设备属性值,用户可在此处对收到的属性值进行处理。
    static int user_property_set_event_handler(const int devid, const char *request, const int request_len)
    {
        ...
        property_setting_handle(request, request_len, &msg);
        ...
    }
    
    static int property_setting_handle(const char *request, const int request_len, recv_msg_t * msg)
    {
        ...
        if ((item = cJSON_GetObjectItem(root, "setPropsExtends")) != NULL && cJSON_IsObject(item)) {
            ...
        }
        if ((item = cJSON_GetObjectItem(root, "powerstate")) != NULL && cJSON_IsNumber(item)) {
            //设置powerstate属性处理
            msg->powerswitch = item->valueint;
            msg->all_powerstate = msg->powerswitch;
            ret = 0;
        }
    #ifdef TSL_FY_SUPPORT /* 支持旧版本开关PowerSwitch属性 */
        else if ((item = cJSON_GetObjectItem(root, "PowerSwitch")) != NULL && cJSON_IsNumber(item)) {
            msg->powerswitch = item->valueint;
            ret = 0;
        }
    #endif
        else if ((item = cJSON_GetObjectItem(root, "allPowerstate")) != NULL && cJSON_IsNumber(item)) {
            //设置allPowerstate属性处理
            msg->powerswitch = item->valueint;
            msg->all_powerstate = msg->powerswitch;
            ret = 0;
        }
    #ifdef AOS_TIMER_SERVICE
        else if (((item = cJSON_GetObjectItem(root, "LocalTimer")) != NULL && cJSON_IsArray(item))|| \
            ((item = cJSON_GetObjectItem(root, "CountDownList")) != NULL && cJSON_IsObject(item)) || \
            ((item = cJSON_GetObjectItem(root, "PeriodTimer")) != NULL && cJSON_IsObject(item)) || \
            ((item = cJSON_GetObjectItem(root, "RandomTimer")) != NULL && cJSON_IsObject(item)))
        {
            // Timer service 定时、倒计时相关属性设置的处理
            cJSON_Delete(root);         // Before LocalTimer Handle, Free Memory
            timer_service_property_set(request);
            user_example_ctx_t *user_example_ctx = user_example_get_ctx();
            IOT_Linkkit_Report(user_example_ctx->master_devid, ITM_MSG_POST_PROPERTY,
                    (unsigned char *)request, request_len);
            return 0;
        }
    #endif
        else {
            LOG_TRACE("property set payload is not JSON format");
            ret = -1;
        }
    
        cJSON_Delete(root);
        if (ret != -1)
            send_msg_to_queue(msg);
    
        return ret;
    }
  • 本地通信功能(目前仅云智能App支持)

    本地通信功能介绍,请参见本地通信开发实践

    本地通信功能在文件make.settings中通过宏ALCS_ENABLED来管理。使用IOT_RegisterCallback函数注册ITE_PROPERTY_GET事件,对应回调函数实现为user_property_get_event_handler。此函数中目前已实现的本地通信请求的设备属性如下所示,如果产品需要增加功能,可以相应的增加新属性的处理case。
    #ifdef ALCS_ENABLED
    static int user_property_get_event_handler(const int devid, const char *request, const int request_len, char **response,
            int *response_len)
    {
        user_example_ctx_t *user_example_ctx = user_example_get_ctx();
        device_status_t *device_status = &user_example_ctx->status;
        cJSON *request_root = NULL, *item_propertyid = NULL;
        cJSON *response_root = NULL;
    
        ...
    
        for (int index = 0; index < cJSON_GetArraySize(request_root); index++) {
            item_propertyid = cJSON_GetArrayItem(request_root, index);
            ...
            LOG_TRACE("Property ID, index: %d, Value: %s", index, item_propertyid->valuestring);
            if (strcmp("powerstate", item_propertyid->valuestring) == 0) { 
                //处理新版物模型开关powerstate属性
                cJSON_AddNumberToObject(response_root, "powerstate", device_status->powerswitch);
            }
            else if (strcmp("allPowerstate", item_propertyid->valuestring) == 0) {
                //处理新版物模型allPowerstate属性
                cJSON_AddNumberToObject(response_root,"allPowerstate", device_status->all_powerstate);
            }
    #ifdef TSL_FY_SUPPORT /* support old feiyan TSL */
            else if (strcmp("PowerSwitch", item_propertyid->valuestring) == 0) {
                //兼容旧版本开关PowerSwitch属性
                cJSON_AddNumberToObject(response_root, "PowerSwitch", device_status->powerswitch);
            }
    #endif
    #ifdef AOS_TIMER_SERVICE
            else if (strcmp("LocalTimer", item_propertyid->valuestring) == 0) {
                ...  //处理本地定时LocalTimer
            } else if (strcmp("CountDownList", item_propertyid->valuestring) == 0) {
                ...  //处理倒计时
    #endif
            }
        }
    
        ...
    }
    #endif
  • 云端解绑与恢复出厂默认设置通知
    设备被解绑后,云端会下发一个解绑事件通知{"identifier":"awss.BindNotify","value":{"Operation":"Unbind"}} 。设备收到此消息可以做重置配网、清空本地数据等处理。如果通过App将设备恢复出厂默认设置,云端会下发一个Reset事件通知{"identifier":"awss.BindNotify","value":{"Operation":"Reset"}} 。设备收到此消息可以做重置配网、清空本地数据等处理。您可以结合具体产品类型,决定收到解绑和恢复出厂默认设置通知后做哪些清空操作。更多介绍,请可以参见示例代码example/smart_outlet/smart_outlet_main.c中的notify_msg_handle函数。
    static int notify_msg_handle(const char *request, const int request_len)
    {
        ....
        if (!strcmp(item->valuestring, "awss.BindNotify")) {
            cJSON *value = cJSON_GetObjectItem(request_root, "value");
            if (value == NULL || !cJSON_IsObject(value)) {
                cJSON_Delete(request_root);
                return -1;
            }
            cJSON *op = cJSON_GetObjectItem(value, "Operation");
            if (op != NULL && cJSON_IsString(op)) {
                if (!strcmp(op->valuestring, "Bind")) {     //绑定通知
                    LOG_TRACE("Device Bind");
                    vendor_device_bind();                   //设备绑定时需要完成的操作,设备应用可定义
                } else if (!strcmp(op->valuestring, "Unbind")) {    //解绑通知
                    LOG_TRACE("Device unBind");
                    vendor_device_unbind();                 //设备解绑时需要完成的操作,设备应用可定义
                } else if (!strcmp(op->valuestring, "Reset")) {     //重置通知
                    LOG_TRACE("Device reset");
                    vendor_device_reset();                  //设备重置时需要完成的操作,设备应用可定义
                }
            }
        }
        ....
    }
  • 蓝牙辅助配网
    蓝牙辅助配网设备端开发,请参见设备端开发
    说明 SDK V1.6.6开始支持新的蓝牙辅助配网方案(配合天猫精灵App V4.13.0以上版本与云智能App V3.5.5以上版本使用),新方案要求设备证书的Device Name与Wi-Fi MAC保持一致。更多介绍,请参见开发自有品牌项目插座产品开发天猫精灵生态项目插座产品
  • 设备端上定时功能

    平台统一使用设备端上定时(DeviceTimer)开发设备端定时功能,详细操作,请参见开发设备端本地定时功能