前言


之前我们介绍了如何在涂鸦智能平台创建智能设备, 以及开发环境搭建, 今天我们就来分析源码,一步步了解对接涂鸦云过程以及API接口,文章所涉及的工程源码,我会上传到我的github:
https://github.com/astonb/tuya_platform,需要的小伙伴自行下载。喜欢diy的小伙伴可以参考此工程,结合以下的源码分析,可以改造成自己的智能设备,实现远程控制。好了,进入我们的正题~

分析

首先需要知道应用入口函数是: OPERATE_RET device_init(VOID) ,在device.c文件中。

Kotlin
        OPERATE_RET device_init(VOID)
{
        OPERATE_RET op_ret;
        
        PR_NOTICE("fireware info name:%s version:%s", APP_BIN_NAME, USER_SW_VER);

        //Product_Init();//1. 涂鸦设备初始化, 参数: pid, app控制回调
        op_ret = tuya_device_init(PRODECT_KEY, device_cb, USER_SW_VER);
        if (op_ret != OPRT_OK)
        {
                return op_ret;
        }
        
        tuya_active_reg(syn_active_data);

        op_ret = sys_add_timer(syn_timer_cb, NULL, &Syntimer);
    if(OPRT_OK != op_ret)
        {
                PR_ERR("add syn_timer err");
            return op_ret;
    }
        else
        {
            sys_start_timer(Syntimer, SYN_TIME*1000, TIMER_CYCLE);
    }

        Product_Init();
        
        op_ret = device_differ_init();
        if (op_ret != OPRT_OK)
        {
                return op_ret;
        }

        return op_ret;
}

分析:

tuya_device_init(PRODECT_KEY, device_cb, USER_SW_VER);

功能:该函数主要完成涂鸦设备初始化。

参数1:设备pid,pid在涂鸦平台创建智能设备时获取,大家根据自己创建的设备为准

forum.jpg


参数2:在app操作,下发指令给设备的回调函数:

C++
VOID device_cb(SMART_CMD_E cmd,cJSON *root){
        CHAR *buf = cJSON_PrintUnformatted(root);
        if (NULL == buf)
        {
                PR_ERR("malloc error");
                return;
        }

        PR_DEBUG("root cmd:%s",buf);

        //实际控制设备
        device_ControlByApp(root);
        
        Free(buf);
}

如:上述例子中的, 将收到的指令传递给device_ControlByApp(root)函数,完成实际对设备的控制:

Markdown
/*********
*  Function: Motor_ControlByApp
*  Description: 接受app控制处理函数
*  Input: root命令
*  Output:
*  Return:
*********/
void device_ControlByApp(cJSON *root)
{        
        cJSON *cmd = cJSON_GetObjectItem(root, DP_control);        //DP_control是在涂鸦平台定义的数据点        if(cmd != NULL){                switch_dev.statu = (bool)cmd->valueint;                        tuya_write_gpio_level(WF_SWITCH_GPIO_PIN, switch_dev.statu);        }        cmd = cJSON_GetObjectItem(root, DP_delayoff);////DP_delayoff是在涂鸦平台定义的数据点        if(cmd != NULL){                switch_dev.delayoff = cmd->valueint;        }        PostSemaphore(Upload_SemHandle);
}

说明:DP_control 和 DP_delayoff 是在涂鸦平台定义的设备数据点:

forum.jpg


forum.jpg


PostSemaphore(Upload_SemHandle); 是 二值信号量(作用:任务同步,因为我们在另外开了一个任务,专门用于上报设备状态,如下代码),用于上报设备状态的, 这里我们控制完设备后, 就应该主动上报下设备状态,以便app能实时。

C++
//================上报设备状态的任务====================void MSG_Upload_Poll_Task(PVOID pArg){        
        OPERATE_RET ret;
        
  //创建信号量
        Upload_SemHandle = CreateSemaphore();
        if(Upload_SemHandle == NULL)
        {
                PR_ERR("Create Upload_Sem failed");        
                ThrdJoin(Upload_TaskHandle, NULL);
                return;
        }
  //初始化为二值信号量,且初始化值为0
        InitSemaphore(Upload_SemHandle, 0, 1);
        
  //创建互斥量,用于上报设备状态时加锁(这里可以省去,不影响)
        ret = CreateMutexAndInit(&Upload_mutexHandle);
        if(ret != OPRT_OK)
        {
                PR_ERR("Create Upload_Mutex failed");        
                ThrdJoin(Upload_TaskHandle, NULL);
                return;
        }
        
        while(1)
        {
    //一直等待信号量,等到了就上报设备状态if(WaitSemaphore(Upload_SemHandle) == OPRT_OK)
                {
                        ret = msg_upload_proc();
                        if(ret != OPRT_OK)
                                PR_ERR("msg_upload failed!");
                }
               
        }
}

其中msg_upload_proc();就是实现设备状态上报:

Markdown
/*********
*  Function: msg_upload_proc
*  Description: dp上报函数
*  Input:
*  Output:
*  Return:
*********/
INT msg_upload_proc(void)
{
        OPERATE_RET err;

  //判断连接涂鸦云的状态,如果是断开的,也就没必要上报了
        if(tuya_get_cloud_stat() == FALSE)                return OPRT_DP_REPORT_CLOUD_ERR;        MutexLock(Upload_mutexHandle);
//判断wifi连接状态,如果不是正常连接,那也没必要上报了
    GW_WIFI_STAT_E wf_stat = tuya_get_wf_status();    if(STAT_UNPROVISION == wf_stat || \       STAT_STA_UNCONN == wf_stat || \       (tuya_get_gw_status() != STAT_WORK)) {                err = OPRT_WF_CONN_FAIL;                goto exit;    }

//================以下是设备数据点格式转化==================
        cJSON *root = cJSON_CreateObject();    if(NULL == root) {                err = OPRT_BUF_NOT_ENOUGH;        goto exit;    }        //根据实际情况上报设备数据点        cJSON_AddBoolToObject(root, DP_control, switch_dev.statu);        cJSON_AddNumberToObject(root, DP_delayoff, switch_dev.delayoff);            CHAR *out = cJSON_PrintUnformatted(root);    cJSON_Delete(root);    if(NULL == out) {        PR_ERR("cJSON_PrintUnformatted err:");                    err = OPRT_CJSON_PARSE_ERR;        goto exit;    }            PR_DEBUG("out[%s]", out);
//=======================end===========================
//最终调用该函数实现数据点上报
    OPERATE_RET op_ret = tuya_obj_dp_trans_report(out);    Free(out);        if( OPRT_OK == op_ret ) {                err = OPRT_OK;        goto exit;    }else {                PR_DEBUG("msg upload error");                err = OPRT_DP_REPORT_CLOUD_ERR;               goto exit;    }

exit:

        MutexUnLock(Upload_mutexHandle);        return err;
}

以上我们完成了设备控制和状态上报功能。

回到device_init用户

主函数

中,接着继续分析下面的代码:

tuya_active_reg(syn_active_data);

该函数作用是:当设备连接涂鸦云成功激活的回调函数,主要是上报下设备状态,相当于设备重新上电启动连接云端时,同步下app显示的设备状态。

参数: syn_active_data

Plain Text
STATIC VOID syn_active_data(VOID)
{
        PR_DEBUG("sys active data");
        if(tuya_get_cloud_stat())
                PostSemaphore(Upload_SemHandle);
        elsePR_DEBUG("cloud unconnect !!!");
}

其函数主要是判断下设备是否成功连接云端,如果不是,则直接退出,如果是,则上报设备状态。

继续device_init代码块:

C++
op_ret = sys_add_timer(syn_timer_cb, NULL, &Syntimer);
    if(OPRT_OK != op_ret)
        {
                PR_ERR("add syn_timer err");
            return op_ret;
    }
        else
        {
            sys_start_timer(Syntimer, SYN_TIME*1000, TIMER_CYCLE);
    }

以上是开启一个定时器, 每2秒(SYN_TIME*1000)去查询连接云端状态,我们看下定时器的回调函数:

Plain Text
STATIC VOID syn_timer_cb(UINT timerID,PVOID pTimerArg)
{
        PR_DEBUG("syn timer cb ...");

        if( FALSE == tuya_get_cloud_stat() ) {
        return;
    }

        PostSemaphore(Upload_SemHandle);
         sys_stop_timer(Syntimer);
}

可以看到其作用和上面的syn_active_data一样, 只是定时器是主动查询。

Product_Init();

C++
void Product_Init(void){
  //设备初始化memset(&switch_dev, 0x0, sizeof(switch_dev));

        //设置GPIO
        PIN_FUNC_SELECT(WF_SWITCH_GPIO_MUX, WF_SWITCH_GPIO_FUNC);
        GPIO_DIS_OUTPUT(WF_SWITCH_GPIO_PIN);//设置成输入模式//内部实现了按键轮询事件(实现了一键配网功能, 具体见代码)
        Key_Init();

        OPERATE_RET op_ret;
//用于延迟关闭检测定时器
        op_ret = sys_add_timer(Timer_delayoff, NULL, &delayoffTimer);
    if(OPRT_OK != op_ret) {
                PR_ERR("add Timer_TaskKEY err");
            //return op_ret;
    }else{
                sys_start_timer(delayoffTimer, 1000, TIMER_CYCLE);
        }
            
}

该函数是我们自己定义实现的,其函数内部主要是初始化了相关的gpio, 以及按键轮询事件定时器,主要是为了实现一键配网功能(以便更换一个网络,再次对设备进行配网)

其中switch_dev是设备属性的结构体,用于管理设备(我这里是开关为例子),

forum.jpg


device_init

主函数

中的最后一个函数device_differ_init:

Nginx
op_ret = device_differ_init();
        if (op_ret != OPRT_OK)
        {
                return op_ret;
        }

其作用是设备的一些个性化初始化, 其实和上面的Product_Init()函数一样,只是涂鸦sdk规范了结构而已。 具体看代码:

C++
STATIC OPERATE_RET device_differ_init(VOID){
        OPERATE_RET op_ret;
        TIMER_ID timer;

        CreateAndStart(&Upload_TaskHandle, MSG_Upload_Poll_Task, NULL, 2048, TRD_PRIO_2, "msg_upload_task");
        
        op_ret = sys_add_timer(wfl_timer_cb, NULL, &timer);
        if (OPRT_OK != op_ret)
        {
                return op_ret;
        }
        else
        {
                sys_start_timer(timer, 300, TIMER_CYCLE);
        }
        
        return OPRT_OK;
}

可以看到在这里我们创建了上面提及的设备状态上报任务MSG_Upload_Poll_Task, 以及创建了一个wifi状态定时查询的定时器:

PHP
STATIC VOID wfl_timer_cb(UINT timerID, PVOID pTimerArg)
{
        STATIC UINT last_wf_stat = 0xffffffff;
        GW_WIFI_STAT_E wf_stat = tuya_get_wf_status();

        //The system is about to restart without monitoring!if (last_wf_stat != wf_stat)
        {
                PR_DEBUG("wf_stat:%d",wf_stat);
                switch (wf_stat)
                {
                        case STAT_UNPROVISION:
                                PR_DEBUG("STAT_UNPROVISION");
                                break;
                        case STAT_AP_STA_UNCONN:
                        case STAT_AP_STA_CONN:
                                PR_DEBUG("STAT_AP_STA_UNCONN");
                                break;
                        case STAT_STA_CONN:
                                PR_DEBUG("STAT_STA_CONN");
                                break;
                        default:
                                break;
                }
                last_wf_stat = wf_stat;
        }
}

在wifi定时查询状态回调中,可以实现不同状态,不同led状态显示, 方便用户根据led状态,知道设备目前是处于啥情况,是断网了呢, 还是成功连接着路由器,还是断开了云端等等(根据自己的情况处理即可)。

编译下载,控制设备

至此,主线的代码已经简单分析完了,剩余一些,如一键配网等函数,很简单,大家可以自行查阅代码,有任何不明白的都可私信或直接在github中提问都可。接下来我们就编译我们的工程,

根据上一篇的开发环境搭建,我们打开虚拟机,进入到sdk下的app,输入以下指令:

CSS
sh build_app.sh wifi_switch 1.0.0

编译过程:

forum.jpg


可以到sdk路径/bin/upgrade目录中,查看我们编译得到的固件

forum.jpg


说明:wifi_switch(1)_1.0.0.bin 是 烧录固件

wifi_switch(2)_1.0.0.bin 是OTA在线升级固件


接着我们烧录我们的固件,打开乐鑫的下载工具:
ESPFlashDownloadTool_v3.6.4.exe

根据以下设置(TYWE1S模组,若使用其他模组,根据实际情况下载):

forum.jpg


好了,看到“FINSH 完成”就表明下载成功, 接着下载涂鸦智能app(可到涂鸦官网或手机应用搜索”涂鸦智能“自行下载,这里不方便贴出二维码~~)

打开app后,我们可以使用“手动搜索” 或 ”自动搜索“ 添加设备(当然此时设备要上电啦~)

forum.jpg


成功添加后,可以进入我们的设备控制主界面:

forum.jpg


之后我们就可以进行控制啦~ (同时我们可以将设备连接电脑串口,观察app控制时,串口接收到数据)

forum.jpg


小结

好了,以上就是涂鸦sdk源码分析和下载验证,相信经过上面的介绍,小伙伴参看代码可以一步步实现自己的智能设备啦,开始行动吧~, 当然有什么不明白的地方,可以私信或下方评论。 另外如果大家对sdk这种方式理解有点困难(涉及一些操作系统知识,如信号量,互斥量,任务等概念), 之后我会另开一篇文章介绍如何使用mcu串口对接, 方便大家更好的入手。 OK, 今天先到此,感兴趣的小伙伴记得关注,收藏,转发~