树莓派如何与物联网平台交互(搭建一个树莓派网关)

更新时间Invalid date

概况

树莓派网关采集 RS-485 温湿度传感器以及 RS-485 门磁开关状态数据,上报到涂鸦云平台。同时,接收云端指令,树莓派网关处理之后,控制继电器动作,继电器返回当前的状态给云端。

步骤

  • 第 1 步:硬件准备

    树莓派(Pi4B)

    树莓派

    树莓派有两个串口可以使用,一个是硬件串口(/dev/ttyAMA0),另一个是 mini 串口(/dev/ttyS0)。

    • 硬件串口有单独的波特率时钟源,性能好,稳定性强。
    • mini 串口功能简单,稳定性较差,波特率由 CPU 内核时钟提供,受内核时钟影响。
    • 树莓派(3/4 代)板载蓝牙模块,默认的硬件串口是分配给蓝牙模块使用的,而性能一般的 mini 串口是分配给 GPIO 串口 TXD0、RXD0。

    在默认状态下,serial0(即 GPIO14,GPIO15)映射到 ttyS0(即 mini 串口:/dev/ttyS0)。ttyS0 的特点是其工作时钟来自于 CPU,CPU 的时钟是从 600MHZ 到 1.5Ghz 动态变化的,所以该串口经常会因为时钟频率变化而发生错误,因此本文没有用此串口和子设备通信。

    serial1(跟板载蓝牙相连)映射到 ttyAMA0,ttyAMA0 是硬件串口,它的时钟频率不受 CPU 影响,因此本文树莓派和子设备通信采用了硬件串口(ttyAMA0)。通过配置交换映射关系,把硬件串口 ttyAMA0 映射到(GPIO14,GPIO15)上。

    最终映射结果可以通过执行命令:ls -l /dev 来查看,发现串口 ttyAMA0 设备节点以及 ttyS0 设备节点已经交换。

    映射结果

    RS-485 转 TTL 模块

    RS-485 转 TTL 模块

    DIR 管脚是控制 RS-485 转 TTL 模块发送与接收。当 DIR 高电平时为发送模式,当 DIR 为低电平时为接收模式。

    路由器(可选)

    树莓派可以连接路由器,也可以连接手机 Wi-Fi 热点,打开 SSH 服务。具体信息,可以参考下文所述登录树莓派的方法。

    继电器模块(RS-485 通信)

    继电器模块

    温湿度传感器(RS-485 通信)

    温湿度传感器

    门磁开关(RS-485 通信)

    Modbus RTU 9600

    image-20211209185744393

    电源设备

    使用树莓派配置的电源插座。

  • 第 2 步:硬件连接

    硬件连接

  • 第 3 步:登录树莓派

    如果没有屏幕,就必须通过网络来登录和控制树莓派电脑,因此必须要有网络支持。可以通过手机开启 Wi-Fi 热点,修改树莓派的配置文件来连接手机网络,并且打开 SSH 服务(笔记本电脑也要连接手机的 Wi-Fi 热点)或者把树莓派通过网线连接到路由器,电脑连接路由器的无线网,使得树莓派和电脑在一个局域网内,并且打开 SSH 服务。下面分享两种基于 SSH 网络登录树莓派的方式。

    利用路由器搭建局域网,登录树莓派

    1. 用路由器搭建局域网,电脑以无线或有线方式连接路由器,树莓派用网线连接路由器。一般来说,Raspberry 会配置为 DHCP,自动获取 IP 地址。
    2. 登录路由器,查看 Raspberry 的 IP 地址,在电脑的 CMD 下 ping 该 IP 地址,能够 ping 通。
    3. 在 SSH 客户端,输入该 IP 地址和端口号 22,登录 Raspberry,输入用户名和密码即可。

    电脑无线端口共享给有线端口,登录树莓派

    如果没有路由器,或者已连接但无法知道树莓派的确切 IP 地址,该怎么办?可以通过电脑的双网卡进行分配。一般来说,现在的电脑都支持双网卡,一个有线网卡、一个无线网卡。可以直接用网线连接电脑网口和树莓派网口,配置电脑的无线接口共享给有线网卡,并对有线网卡进行网络共享。前提是需要设置有线网卡的 IPV4 为 DHCP 模式。具体步骤如下:

    1. 打开 网络和 Internet 选项 > 更改适配器 选项 > WLAN 属性 > 共享 > 允许其他用户通过此计算机的 Internet 连接来连接 > 选择有线网卡。手机开启 Wi-Fi 热点连接时,选择以太网。
    2. 设置有线网卡 IPv4,自动获取 IP 地址。
    3. cmd 输入 arp -a,查看以 192.168.137.1 为网关的列表中出现的树莓派端口。通过插拔网线的方式,查看新出现的 IP 地址,即树莓派的 IP 地址。
      ​4. 启用 SSH,端口 22,访问 IP 地址为 192.168.137.239
    4. 连接 SSH,输入用户名和密码即可。
  • 第 4 步:产品创建

    需要在 涂鸦 IoT 开发平台 创建产品,获取授权信息,然后将产品和授权相关信息写入到代码中,接入涂鸦云。详细步骤如下:

    创建网关设备

    1. 登录 涂鸦 IoT 开发平台

    2. 单击 创建产品 并 选择 行业解决方案 > 智慧工业 > 工业网关 品类。

      创建产品

    3. 单击 生态设备接入

      生态设备接入

    4. 选择 TuyaLink 自定义方案。

    5. 输入产品名称,设备类型选择 网关设备,数据协议选择 涂鸦标准协议,通讯协议选择 以太网,并单击 创建产品

      网关设备

    6. 功能定义 界面,单击 添加功能 并填写相关参数,完成产品功能定义。根据要实现的设备功能,创建 DP 功能点。

      添加功能

    7. 设备开发 界面,选择并下载 SDK 方案。

      设备开发

    8. 单击 下一步,进入激活信息获取页面。涂鸦提供免费的授权码供测试使用,您可以免费领取 2 个激活码。领取成功后,单击 注册设备,即完成设备注册。

      激活码

      设备相应信息会显示在下方。

      设备信息

    9. 将注册的设备信息,填写到 examples/data_model_basic_demo/data_model_basic_demo.c 文件中,编译并运行 Demo 即可连接涂鸦 IoT 云。关于编译的具体流程,请参考下文 编译执行 章节内容。

      编译

      改之前:
      const char productId[] = "3jbcpefnn1jxxxxx";
      const char deviceId[] = "6ced2aa564727c01xxxxx";
      const char deviceSecret[] = "ac5d367db39xxxxx";
      改之后:
      const char productId[] = "t1tlm6p13aoxxxxx";
      const char deviceId[] = "6cf918e90b12f7b1fxxxxx";
      const char deviceSecret[] = "a5f23a3fb34xxxxx";
      注意:根据自己创建的网关产品 PID、设备 ID 和设备密钥进行更改。
      

    创建 RS-485 子设备

    子设备的创建流程同上,只有操作到下图的步骤时,按照下图圈出来的更改即可。

    子设备

    子设备创建完成后如下图,在网关设备创建的时候,DP 点已经创建,这里无需再次创建。

    继电器:
    继电器

    温湿度传感器:
    温湿度传感器

    门磁传感器:
    门磁传感器

  • 第 5 步:程序设计

    程序设计大致分为两部分,一部分是树莓派和 RS-485 子设备通信的程序,另外一部分是树莓派和涂鸦云平台进行交互的程序。

    连接涂鸦云平台

    程序设计简单概述

    树莓派和涂鸦云平台进行数据交互的时候采用 C 语言编写。

    程序设计逻辑分析

    在平台上创建网关设备时,下载 C TuyaLink SDK 开发包,在此 demo 上移植自己想要实现的功能。

    data_model_basic_demo.c 实现整个控制逻辑:

    #include <assert.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <pthread.h>  
    
    #include "cJSON.h"
    #include "tuya_cacert.h"
    #include "tuya_log.h"
    #include "tuya_error_code.h"
    #include "system_interface.h"
    #include "mqtt_client_interface.h"
    #include "tuyalink_core.h"
    
    const char productId[] = "t1tlm6p13aouh***";
    const char deviceId[] = "6cf918e90b12f7b1ffw***";
    const char deviceSecret[] = "a5f23a3fb341e***";
    
    tuya_mqtt_context_t client_instance;
    
    // 写文件
    int write_file(char str[])
    {
        FILE *fp = NULL; 
        fp = fopen("/tmp/from_platform.txt", "w");
        fprintf(fp, "%s", str);
        fclose(fp);
        printf("deviceid_action_time: %s\n", str); 
        return 0;
    }
    
    // 读文件
    int read_file(int num,char return_data[])
    {
        FILE *fp = NULL;
      switch (num)
        {
        case 1:
            fp = fopen("/tmp/1.txt", "r");
            if(fp == NULL)
            {
               printf("open /tmp/1.txt error!\n");
               return 0;
            }
            fgets(return_data, 255, (FILE*)fp);
            fclose(fp);
            break;
        case 2:
            fp = fopen("/tmp/2.txt", "r");
            if(fp == NULL)
            {
               printf("open /tmp/2.txt error!\n");
               return 0;
            }
            fgets(return_data, 255, (FILE*)fp);
            fclose(fp);
            break;
        case 3:
            fp = fopen("/tmp/3.txt", "r");
            if(fp == NULL)
            {
               printf("open /tmp/3.txt error!\n");
               return 0;
            }
            fgets(return_data, 255, (FILE*)fp);  
            fclose(fp); 
            break;
        default:
            break;
        }
        printf("return_data:%s\n",return_data);
        return 0;
    }
    
    // 写一个线程函数 void *函数名(void *arg)
    void *thread_worker1(void *arg) 
    {  
        char json_temp_hum[255];
        char json_door_state[255];
        char json_data[255];
        char json_data1[255];
            while(1)
            {  
                if(read_file(2,json_data) == 0)
                {
                    sprintf(json_temp_hum,"%s",json_data);
                    printf("json_temp_hum:%s\r\n",json_temp_hum);
                    tuyalink_thing_property_report_with_ack(arg, NULL, json_temp_hum);
                    memset(json_data, 0, sizeof(json_data));  
                    
                }
                else
                {
                    printf("read temp_hum error\r\n");
    
                }
                if(read_file(3,json_data1) == 0)
                {
                    sprintf(json_door_state,"%s",json_data1);
                    printf("json_door_state:%s\r\n",json_door_state);
                    tuyalink_thing_event_trigger(arg, NULL, json_door_state); 
                    memset(json_data1, 0, sizeof(json_data1));  
                }
                else
                {
                    printf("read door_state error\r\n");
    
                }
                sleep(2); 
            }  
    }
    
    void on_connected(tuya_mqtt_context_t* context, void* user_data)
    { 
        int error=0;
        pthread_t t1;
        tuyalink_subdevice_bind(context, "[{\"productId\":\"snigjkwkheaxueqa\",\"nodeId\":\"255\",\"clientId\":\"1\"}]");//继电器
        tuyalink_subdevice_bind(context, "[{\"productId\":\"dtoqgbr5azgwvga3\",\"nodeId\":\"254\",\"clientId\":\"2\"}]");//门磁
        tuyalink_subdevice_bind(context, "[{\"productId\":\"6jmmnuwavyxkcv1x\",\"nodeId\":\"1\",\"clientId\":\"3\"}]");//温湿度传感器
        error=pthread_create(&t1,NULL,thread_worker1,context);
        if(error)
           {
              printf("create pthread error!\n");
              return;     
           } 
        
    }
    
    void on_disconnect(tuya_mqtt_context_t* context, void* user_data)
    {
        TY_LOGI("on disconnect");
    }
    
    
    void on_messages(tuya_mqtt_context_t* context, void* user_data, const tuyalink_message_t* msg)
    {
        char json_relay_state[255];
        TY_LOGI("on message id:%s, type:%d, code:%d", msg->msgid, msg->type, msg->code);
        switch (msg->type) {
            case THING_TYPE_MODEL_RSP:
                TY_LOGI("Model data:%s", msg->data_string);
                break;
    
            case THING_TYPE_PROPERTY_SET:
                TY_LOGI("property set:%s", msg->data_string);
                break;
    
            case  THING_TYPE_PROPERTY_REPORT_RSP:
               break;
    
            case THING_TYPE_ACTION_EXECUTE:
                  TY_LOGI("action execute:%s", msg->data_string);
                  if(write_file(msg->data_string) == 0)
                     {
                        printf ("write ok\r\n");
                        printf ("data_string:%s\r\n",msg->data_string);
                     }
                 else
                    {
                        printf ("write error\r\n");
                    }
                     if(read_file(1,json_relay_state) == 0)
                     {
                       sprintf(msg->data_string,"%s",json_relay_state);
                       printf("json_relay_state:%s\r\n",json_relay_state);
                       tuyalink_thing_property_report(context, NULL, msg->data_string);  
                       memset(json_relay_state, 0, sizeof(json_relay_state));      
                     }
                     else
                     {
                      printf ("read relay_state error\r\n");
                     }     
                break;   
    
            default:
                break;
        }
        printf("\r\n");
    }
    
    int main(int argc, char** argv)
    {
        int ret = OPRT_OK;
        tuya_mqtt_context_t* client = &client_instance;
        ret = tuya_mqtt_init(client, &(const tuya_mqtt_config_t) {
            .host = "m2.tuyacn.com",
            .port = 8883,
            .cacert = tuya_cacert_pem,
            .cacert_len = sizeof(tuya_cacert_pem),
            .device_id = deviceId,
            .device_secret = deviceSecret,
            .keepalive = 60,
            .timeout_ms = 2000,
            .on_connected = on_connected,
            .on_disconnect = on_disconnect,
            .on_messages = on_messages
        });
        assert(ret == OPRT_OK);
        ret = tuya_mqtt_connect(client);
        assert(ret == OPRT_OK);
          for (;;) 
           {
            /* Loop to receive packets, and handles client keepalive */
            tuya_mqtt_loop(client);
           }
        return ret;
    } 
    

    程序函数功能解析

    注意:下文会提到树莓派网关、C 端和 Python 端等。具体含义如下:

    树莓派做成网关,里面跑了两个进程,一个是采用 Python 语言处理子设备数据收发,以及 JSON 数据解析(Python 端)。另外一个进程是采用 C 语言处理,和涂鸦云平台进行交互(C 端)。

    这些是在平台上创建网关设备时的参数,填入对应的位置即可。

    const char productId[] = "t1tlm6p13aouh***";
    const char deviceId[] = "6cf918e90b12f7b1ffw***";
    const char deviceSecret[] = "a5f23a3fb341e***";
    

    在 main 函数里,实例化和初始化一个设备对象 tuya_mqtt_context_t,用来初始化产品 ID 和授权信息等配置参数以及循环接收数据包,并处理客户端保持连接。

    int main(int argc, char** argv)
    {
        int ret = OPRT_OK;
        tuya_mqtt_context_t* client = &client_instance;
        ret = tuya_mqtt_init(client, &(const tuya_mqtt_config_t) {
            .host = "m2.tuyacn.com",
            .port = 8883,
            .cacert = tuya_cacert_pem,
            .cacert_len = sizeof(tuya_cacert_pem),
            .device_id = deviceId,
            .device_secret = deviceSecret,
            .keepalive = 60,
            .timeout_ms = 2000,
            .on_connected = on_connected,
            .on_disconnect = on_disconnect,
            .on_messages = on_messages
        });
        assert(ret == OPRT_OK);
        ret = tuya_mqtt_connect(client);
        assert(ret == OPRT_OK);
          for (;;) 
           {
            /* Loop to receive packets, and handles client keepalive */
            tuya_mqtt_loop(client);
           }
        return ret;
    }       
    

    启动 TuyaOS SDK 服务。

    ret = tuya_mqtt_connect(client);
    //TuyaOS SDK 服务任务,数据接收处理,设备在线保活等任务处理
    

    循环调用将当前线程产生给底层的 Link SDK 客户端。

    tuya_mqtt_loop(client);
    

    定义应用层事件回调,on_messages 回调函数用于应用层接收 SDK 事件通知,如数据功能点(DP)下发,云端连接状态通知。平台下发指令在此函数中实现。

    void on_messages(tuya_mqtt_context_t* context, void* user_data, const tuyalink_message_t* msg)
    {
        char json_relay_state[255];
        TY_LOGI("on message id:%s, type:%d, code:%d", msg->msgid, msg->type, msg->code);
        switch (msg->type) {
            case THING_TYPE_MODEL_RSP:
                TY_LOGI("Model data:%s", msg->data_string);
                break;
    
            case THING_TYPE_PROPERTY_SET:
                TY_LOGI("property set:%s", msg->data_string);
                break;
    
            case  THING_TYPE_PROPERTY_REPORT_RSP:
               break;
    
            case THING_TYPE_ACTION_EXECUTE:
                  TY_LOGI("action execute:%s", msg->data_string);
                  if(write_file(msg->data_string) == 0)
                     {
                        printf ("write ok\r\n");
                        printf ("data_string:%s\r\n",msg->data_string);
                     }
                 else
                    {
                        printf ("write error\r\n");
                    }
                     if(read_file(1,json_relay_state) == 0)
                     {
                       sprintf(msg->data_string,"%s",json_relay_state);
                       printf("json_relay_state:%s\r\n",json_relay_state);
                       tuyalink_thing_property_report(context, NULL, msg->data_string);  
                       memset(json_relay_state, 0, sizeof(json_relay_state));      
                     }
                     else
                     {
                      printf ("read relay_state error\r\n");
                     }     
                break;   
    
            default:
                break;
        }
        printf("\r\n");
    }
    

    THING_TYPE_ACTION_EXECUTE 主题中,树莓派网关从平台上获取的指令(JSON 格式)存入文件中,供 Python 端调用(Python 端处理后控制继电器动作)。树莓派网关同时从相应文件中读取设备数据值(Python 端获取设备值,处理成 JSON 格式存入文件中供 C 端调用),C 端处理成 JSON 格式的字符串上报到云端。

     case THING_TYPE_ACTION_EXECUTE:
                  TY_LOGI("action execute:%s", msg->data_string);
                  if(write_file(msg->data_string) == 0)
                     {
                        printf ("write ok\r\n");
                        printf ("data_string:%s\r\n",msg->data_string);
                     }
                 else
                    {
                        printf ("write error\r\n");
                    }
                     if(read_file(1,json_relay_state) == 0)
                     {
                       sprintf(msg->data_string,"%s",json_relay_state);
                       printf("json_relay_state:%s\r\n",json_relay_state);
                       tuyalink_thing_property_report(context, NULL, msg->data_string);  
                       memset(json_relay_state, 0, sizeof(json_relay_state));      
                     }
                     else
                     {
                      printf ("read relay_state error\r\n");
                     }     
                break;   
    

    网关发现子设备,请求云端激活子设备并建立 topo 关系。适用于设备无法预先在云端注册,也无法烧录,网关发现子设备后,请求云端注册并绑定到当前网关下。在 MQTT 连接成功回调函数里面绑定了三个子设备。

    • 子设备的 productId 从平台上获取。
    • nodeId 是设备的节点 ID。至少保证网关下唯一,可以是子设备的地址。
    • clientId 是设备端唯一 ID,子设备硬件的唯一标识。可以是设备的 UUID、MAC、SN 等,至少保证产品下唯一。

    ID

    在 MQTT 连接成功回调函数中创建了一个子线程。

    子线程

    线程函数具体实现功能:用于不断获取温湿度数据以及门磁状态数据,同时上报到云平台。

    //写一个线程函数 void *函数名(void *arg)
    void *thread_worker1(void *arg) 
    {  
        char json_temp_hum[255];
        char json_door_state[255];
        char json_data[255];
        char json_data1[255];
            while(1)
            {  
                if(read_file(2,json_data) == 0)
                {
                    sprintf(json_temp_hum,"%s",json_data);
                    printf("json_temp_hum:%s\r\n",json_temp_hum);
                    tuyalink_thing_property_report_with_ack(arg, NULL, json_temp_hum);
                    memset(json_data, 0, sizeof(json_data));  
                    
                }
                else
                {
                    printf("read temp_hum error\r\n");
    
                }
                if(read_file(3,json_data1) == 0)
                {
                    sprintf(json_door_state,"%s",json_data1);
                    printf("json_door_state:%s\r\n",json_door_state);
                    tuyalink_thing_event_trigger(arg, NULL, json_door_state); 
                    memset(json_data1, 0, sizeof(json_data1));  
                }
                else
                {
                    printf("read door_state error\r\n");
    
                }
                sleep(2); 
            }  
    }
    

    下面是读文件函数,主要用于读取 Python 端获取子设备的数据值,然后放于数组 return_data 中。read_file 函数传入了两个参数,一个是 num,用于区分读取的文件(三个文件分别存入三个不同设备的数据值)。另外一个是数组 return_data,用于存储读取文件的数据(供 C 端调用)。

       //读文件
    int read_file(int num,char return_data[])
    {
        FILE *fp = NULL;
      switch (num)
       {
        case 1:
            fp = fopen("/tmp/1.txt", "r");
            if(fp == NULL)
            {
               printf("open /tmp/1.txt error!\n");
               return 0;
            }
            fgets(return_data, 255, (FILE*)fp);
            fclose(fp);
            break;
        case 2:
            fp = fopen("/tmp/2.txt", "r");
            if(fp == NULL)
            {
               printf("open /tmp/2.txt error!\n");
               return 0;
            }
            fgets(return_data, 255, (FILE*)fp);
            fclose(fp);
            break;
        case 3:
            fp = fopen("/tmp/3.txt", "r");
            if(fp == NULL)
            {
               printf("open /tmp/3.txt error!\n");
               return 0;
            }
            fgets(return_data, 255, (FILE*)fp);  
            fclose(fp); 
            break;
        default:
            break;
        }
        printf("return_data:%s\n",return_data);
        return 0;
    }
    

    下面是写文件函数,主要存储从云端获取的指令,然后供 Python 端调用。

    //写文件
    int write_file(char str[])
    {
        FILE *fp = NULL; 
        fp = fopen("/tmp/from_platform.txt", "w");
        fprintf(fp, "%s", str);
        fclose(fp);
        printf("deviceid_action_time: %s\n", str); 
        return 0;
    }
    

    调试注意事项

    如果调试过程中报段错误,首先应该想到段错误的定义,从它出发考虑引发错误的原因。

    段错误

    • 在使用指针时,定义了指针后记得初始化指针,在使用的时候记得判断是否为 NULL。

    • 在使用数组时,注意数组是否被初始化,数组下标是否越界,数组元素是否存在等。

    • 在访问变量时,注意变量所占地址空间是否已经被程序释放掉。

    • 在处理变量时,注意变量的格式控制是否合理等。

    在调试的过程中,由于没有对打开文件为空时进行处理,导致出现了段错误,如下图所示,修改解决了此问题。

    调试

    RS-485 子设备通信

    程序设计简单概述

    树莓派和 RS-485 子设备通信采用 Python 语言来编写。

    程序设计逻辑分析

    new_temp_hum_door.py 文件中实现整个控制逻辑。

    温湿度传感器和门磁状态获取函数里面引用类(从 new_relay_control 文件中引用 relay),执行相应的控制逻辑。While 循环读取文件中存储的数据(平台下发的指令,JSON 格式),指令解析之后,控制继电器动作以及循环获取温湿度传感器数据和门磁的状态,处理成 JSON 格式的字符串存入文件中(供 C 端读取)。

    # -*- coding: utf-8 -*-
    from new_relay_control import relay
    from time import sleep
    import json
    
    #温湿度获取
    def temp_hum_sensor_get():
        temp_hum = relay()
        temp_hum.all_relay = 3
        temp_hum.relay_all_on_order = ['01 04 00 00 00 02 71 CB']
        return_str = temp_hum.ALL_ON()
        return return_str
    
    #门磁状态获取
    def door_sensor_get():
        door_sensor = relay()
        door_sensor.all_relay = 3 
        door_sensor.relay_all_on_order = ['FE 01 00 00 00 02 A9 C4']
        return_str = door_sensor.ALL_ON()
        return return_str
    
    id_value = 2
    time = 1
    
    while True:
        try:
            #读文件(读平台下发的指令)
            fp = open('/tmp/from_platform.txt', 'r')
            str_read = fp.read()
            # {"inputParams":{"relay_action":true},"actionCode":"relay"}
            recv_json = json.loads(str_read)
            relay_action = recv_json['inputParams']['action']
            action_code = recv_json['actionCode']
            print(relay_action)
            print(action_code)
            print("read from platform: %s " % str_read)
            print("data type: %s" % (type(str_read)))
    
    
    
            if action_code == "relay":
                relay_open = relay()
                if relay_action == True:
                    return_str = relay_open.ALL_ON()
                else:
                    return_str = relay_open.ALL_OFF()
    
                relay_str = return_str[8:12]
                upload_to_platform = 0
                print(relay_str)
                if relay_str != "0000":
                    upload_to_platform = 1
    
                string = "{\"actionCode\": \"relay\", \"actionTime\": 1626197189630,\"outputParams\": {\"relaystate\":0}}"
                new_json = json.loads(string)
                new_json['outputParams']['relaystate'] = upload_to_platform
                final_str3 = json.dumps(new_json)
                print(final_str3)
    
                f = open('/tmp/1.txt', 'w')
                f.write(final_str3)
                f.close()
    
        except FileNotFoundError:
                print ("File is not found")
    
        if id_value == 2:
            return_str = temp_hum_sensor_get()
            print(return_str)
            get_str1 = return_str[6:10]
            get_str2 = return_str[10:14]
                
            try:
                # temp_value = (int(get_str1, 16))/10
                temp_value = int(get_str1, 16)
                # hum_value = (int(get_str2, 16)) / 10
                hum_value = int(get_str2, 16)
            except ValueError:
                pass    
          
            print(temp_value)
            print(hum_value)
            print(get_str1)
            print(get_str2)
    
            string1 = "{\"temp\":{\"value\":\"temp_value\",\"time\":1631708204231},\"hum\":{\"value\":\"hum_value\",\"time\":1631708204231}}"
            new_json1 = json.loads(string1)
            new_json1['temp']['value'] = temp_value
            new_json1['hum']['value'] = hum_value
            final_str4 = json.dumps(new_json1)
            print(final_str4)
    
            f = open('/tmp/2.txt', 'w')
            f.write(final_str4)
            f.close()
    
    
        #if id_value == 3:
            return_str = door_sensor_get()
            get_str3 = return_str[6:8]
            door_upload_to_platform = 0
            if get_str3 == "00":
               door_upload_to_platform = 0
            else:
               door_upload_to_platform = 1
    
            string2 = "{\"eventCode\":\"door\",\"eventTime\":1626197189630,\"outputParams\":{\"doorsate\":0}}"
            new_json2 = json.loads(string2)
            new_json2['outputParams']['doorsate'] = door_upload_to_platform
            final_str5 = json.dumps(new_json2)
            print(get_str3)
            print(final_str5)
            f = open('/tmp/3.txt', 'w')
            f.write(final_str5)
            f.close()
        sleep(time)
    

    使用 class 定义类,实现继电器的控制逻辑。类里面定义了串口收发的函数 relay_send(self, send_order) 以及继电器打开与关闭的控制逻辑。

    # -*- coding: utf-8 -*-
    import RPi.GPIO as GPIO
    import serial
    from time import sleep
    
    '''2路继电器开关控制函数,单独继电器开关控制和全部开关控制'''
        
    class relay(object):
        def __init__(self):
            self.relay_all_on_order = ['02 05 00 00 FF 00 8C 09', '02 05 00 01 FF 00 DD C9', '02 0F 00 00 00 08 01 FF FE C0']
            self.relay_all_off_order = ['02 05 00 00 00 00 CD F9', '02 05 00 01 00 00 9C 39', '02 0F 00 00 00 08 01 00 BE 80']
            self.relay1 = 1
            self.relay2 = 2
            self.all_relay = 3
            self.port = '/dev/ttyAMA0'
            
        def relay_send(self, send_order):
    
           if self.port:
               relay_serial = serial.Serial(self.port, 9600)
               GPIO.setmode(GPIO.BCM)
               GPIO.setup(17, GPIO.OUT)
               if not relay_serial.isOpen():
                   relay_serial.Open()
               while True:
                   GPIO.output(17, GPIO.HIGH)
                   sleep(0.01)
                   relay_serial.write(bytes.fromhex(send_order))
                   #relay_serial.write(bytes(send_order))
                   sleep(0.01)
                   GPIO.output(17, GPIO.LOW)
                   count = relay_serial.inWaiting()
                   if count > 0:
                       GPIO.output(17, GPIO.LOW)
                       sleep(0.01)
                       recv = relay_serial.read(count)
                       GPIO.output(17, GPIO.HIGH)
                       sleep(0.01)
                       print("recv: ", recv)
                       # recv_bytes = binascii.b2a_hex(recv)
                       # recv_str = binascii.b2a_hex(recv_bytes).decode('utf-8')
                       recv_str = str(recv.hex())
                       print("recv_str: ", recv_str)
                       print("recv_str: ", recv_str)
                       if recv_str == "00":
                            print("error")
                       else:
                            return recv_str
                   sleep(0.5)
                   #relay_serial.close()
    
        def ALL_ON(self):
            send_order = self.relay_all_on_order[self.all_relay - 3]
            print(send_order)
            get_return = self.relay_send(send_order)
            print("继电器控制: ALL_RELAY_ON")
            return get_return
    
        def ALL_OFF(self):
            send_order = self.relay_all_off_order[self.all_relay - 3]
            get_return = self.relay_send(send_order)
            print("继电器控制: ALL_RELAY_OFF")
            return get_return
    
        def RELAY1_ON(self):
            send_order = self.relay_all_on_order[self.relay1  - 1]
            get_return = self.relay_send(send_order)
            print("继电器控制: RELAY1_ON")
            return get_return
    
        def RELAY1_OFF(self):
            send_order = self.relay_all_off_order[self.relay1 - 1]
            get_return = self.relay_send(send_order)
            print("继电器控制: RELAY1_OFF")
            return get_return
    
        def RELAY2_ON(self):
            send_order = self.relay_all_on_order[self.relay2 - 1]
            get_return = self.relay_send(send_order)
            print("继电器控制: RELAY2_ON")
            return get_return
    
        def RELAY2_OFF(self):
            send_order = self.relay_all_off_order[self.relay2 - 1]
            get_return = self.relay_send(send_order)
            print("继电器控制: RELAY2_OFF")
            return get_return
    
    if __name__ == "__main__":
        relay = relay()
        relay.port = '/dev/ttyAMA0'
    

    调试注意事项

    子设备通信程序需要在 Python 3 以上版本进行编译运行。如果 Python 是低版本,编译运行时会报错。

    有些写法在低版本里是不支持的。例如下面写法,Python 2.7 版本会报错,Python 3 以上版本则不会。

    temp_value = int(get_str1, 16)
    hum_value = int(get_str2, 16)
    

    下面编码声明在 Python 3 以上版本则不用,在低版本需要声明,不然运行会报错。

    # -*- coding: utf-8 -*-
    

    下面写法同样在 Python 3 以上版本适用,在低版本中则不支持,不然运行会报错。

    recv_str = str(recv.hex())
    

    若出现下面读文件错误,导致编译运行中断,则是由于当文件不存在时,没有进行异常处理。由于收到平台指令才开始创建文件,所以没有指令下发时文件没有创建,导致运行 Python 端程序时会报错。

    报错

    可以参照下图进行解决:(添加捕获异常处理)

    解决

  • 第 6 步:编译执行(Linux)

    1. 安装 make 等相关环境依赖。

      sudo apt-get install make cmake 
      
    2. 新建一个文件夹开始编译。

      mkdir build && cd build
      cmake ..
      make
      

      文件夹

    3. 运行 Demo。

      ./bin/data_model_basic_demo
      

      Demo

    4. 在设备端查看运行接口。以下日志显示设备与涂鸦云平台连接成功。

      连接成功

    5. 设备成功连接到涂鸦云平台后,单击进行刷新,设备状态会显示为在线。

      在线

  • 第 7 步:在线调试

    1. 在平台上,查看上报的消息以及对设备下发相应指令。

      消息

    2. 温湿度值上报云平台(属性)。

      温湿度值

      数据有没有上报成功,看 code 值:

      温湿度值 code

    3. 门磁设备上报数据到云平台(事件)。

      门磁

      数据有没有上报成功,看 code 值:

      门磁 code

    4. 平台下发指令,继电器动作(动作)。

      继电器

  • 第 8 步:结果展示

    结果展示

小结

注意事项

硬件注意事项

  • 树莓派的引脚短路,特别是 VCC 和 GND,短路会造成芯片烧毁无法恢复。

  • 树莓派启动需要几十秒时间,打开电源后 1 分钟内不可以关闭电源,会影响树莓派的使用寿命。

  • SD 卡烧录系统完成时,系统会提示格式化,此时不需要格式化,单击取消即可。若点了格式化后,树莓派会提示缺失文件,需要重新烧录系统。

  • 树莓派 4B 的 HDMI 接口变成两个 micro-HDMI 接口(hdmi0 和 hdmi1),可以接入两个显示器。如果只连接一个显示器,一定要插入 hdmi0 接口,也就是靠近 type-C 电源接口的那一个,才可以正常显示。如果只插入 hdmi1 接口,会出现显示器无法显示的情况。

    • 注意
      HDMI 线反接会导致接口损坏!
  • 拆插设备前,需要先断电。

软件注意事项

  • 由于树莓派跑了两个进程,因此整个过程中需要两个程序同时运行。

  • 软件调试过程中的其它注意事项已在 程序设计 中具体指出。