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

更新时间:2022-09-08 09:16:02

概况

树莓派网关采集 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 线反接会导致接口损坏!
  • 拆插设备前,需要先断电。

软件注意事项

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

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