树莓派+阿里云IoT平台实现实时采集办公室温湿度数据

15

冬季天干物燥,如何实时采集办公室温湿度数据?

--树莓派+阿里云IoT平台实现

一、树莓派简介

树莓派是什么?

树莓派不是一款餐后甜点,而是一个只有信用卡大小的计算机,更准确的说它是一款单板计算机。树莓派由注册于英国的慈善组织 “Raspberry Pi 基金会” 开发和维护,其设计初衷是用来教孩子们学习程序设计的低成本计算机,而现在它已经可以用来做很多有趣的事情。

rasp

上图中列出了树莓派提供的一些接口,比如支持 USB 供电接口、HDMI 连接显示器的接口、摄像头接口、音频输出接口、以太网接口、USB2.0 接口等常用接口,通过这些接口使得树莓派可以无限扩展,甚至可以作为一个完整的 PC 机使用。不过这里我们要说一下上图中树莓派自带的40个GPIO引脚接口。

GPIO针脚

所谓GPIO (General Purpose I/O Ports ,意思为通用输入/输出端口),通俗地说就是一些引脚,可以通过它们输入高(低)电平或者通过它们读取引脚的状态(高电平或低电平)。用户可以通过GPIO接口和硬件进行数据交互(如UART),控制硬件工作(如控制LED灯、蜂鸣器等),读取硬件的工作状态信号(如中断信号)等,GPIO的使用非常广泛,掌握了GPIO,差不多相当掌握了操作硬件的能力。(引脚与实际树莓派对照方式:将4个USB接口正对使用者,40个引脚即为下图所示)

树莓派引脚图2

针脚编码方式:

目前有三种方式对树莓派的引脚进行编码:

① 使用 BOARD编号系统,这种方式参考树莓派主板上接线柱的针脚编号。使用这种方式的优点是无需考虑主板的修订版本,引脚数量增减不影响同编号引脚的功能,编号相同的引脚功能相同。

② 使用 BCM编码,该方式参考Broadcom SOC 的通道编号,编号中也就省去了不能用程序控制的 VCC 和 GND 引脚。使用过程中要保持主板上的针脚与图表上标注的通道编号相对应。

③ 使用 WiringPi编码,它是一个用 C 语言编写的树莓派软件包,功能强大,可用于树莓派GPIO引脚控制、串口通信、SPI 通信及 I2C 通信等功能(不过作者大佬由于其开源版权无法得到尊重以及使用者无数次无效的提问邮件烦扰,其已经宣布软件包不再更新:wiringPi – deprecated…

二、温湿度传感器

温湿度传感器DHT11简介

数字温湿度传感器 DHT11 是一种复合传感器,包含温度和湿度的标准数字信号输出,采用专用的数字模块采集技术和温湿度传感技术,确保产品具有高度可靠性和优异的长期稳定性。

温湿度传感器

该传感器包括一个电阻式感湿元件和一个NTC测温元件,并与一个高性能8位单片机相连接。因此该产品具有品质卓越、超快响应、抗干扰能力强、性价比极高(价格便宜,某宝几块钱一个)等优点。其精度湿度±5%RH, 温度±2℃,量程湿度595%RH, 温度-20+60℃。

DHT11的数据格式

温湿度传感器结构图

DATA 用于树莓派与 DHT11 之间的通讯和同步,采用单总线数据格式,一次通讯时间 4ms 左右,数据分为小数部分、整数部分及校验位部分。一次传输5个字节共40位数据,高位先出。

数据格式:

8bit 湿度整数数据 + 8bit 湿度小时数据 + 8bit 温度整数数据 + 8bit 温度小数数据 + 8bit 校验位

校验位数据定义:

8bit 校验位 等于 8bit 湿度整数数据 + 8bit 湿度小时数据 + 8bit 温度整数数据 + 8bit 温度小数数据 所得结果的末8位

温湿度数据格式示例

详细工作原理可参考附录1。

三、获取温湿度数据

线路连接

树莓派温湿度传感器
GPIO.7DATA
5VVCC
GNDGND

温湿度传感器最左边为DATA接口、中间为VCC接口,最右边为GND接口。

树莓派GPIO接口对应DATA我们取GPIO.7,也就是BOARD编码7号针脚;电源取4号针脚5V接口;GND取6号针脚。

数据线连接

控制程序

编写控制程序,将温湿度传感器中的二进制数据转化为十进制数据,校验后将温湿度数据打印出来。

#!/usr/bin/env python
import RPi.GPIO as GPIO
import numpy as np
import time
 
DHTPIN = 4         #引脚号4
GPIO.setmode(GPIO.BCM)      #以BCM编码格式

def read_dht11_dat():
    GPIO.setup(DHTPIN, GPIO.OUT)
    GPIO.output(DHTPIN, GPIO.LOW)
    #给信号提示传感器开始工作,并保持低电平18ms以上
    time.sleep(0.02)                #这里保持20ms   
    GPIO.output(DHTPIN, GPIO.HIGH)  #然后输出高电平
    
    GPIO.setup(DHTPIN, GPIO.IN)    
    # 发送完开始信号后得把输出模式换成输入模式,不然信号线上电平始终被拉高
 
    while GPIO.input(DHTPIN) == GPIO.LOW:
        continue
    #DHT11发出应答信号,输出 80 微秒的低电平
    
    while GPIO.input(DHTPIN) == GPIO.HIGH:
        continue
    #紧接着输出 80 微秒的高电平通知外设准备接收数据
    
    #开始接收数据
    j = 0               #计数器
    data = []           #收到的二进制数据
    kk=[]               #存放每次高电平结束后的k值的列表
    while j < 40:
        k = 0
        while GPIO.input(DHTPIN) == GPIO.LOW:  # 先是 50 微秒的低电平
            continue
        
        while GPIO.input(DHTPIN) == GPIO.HIGH: # 接着是26-28微秒的高电平,或者 70 微秒的高电平
            k += 1
            if k > 100:
                break
        kk.append(k)
        if k < 8:       #26-28 微秒时高电平时通常k等于5或6
            data.append(0)      #在数据列表后面添加一位新的二进制数据“0”
        else:           #70 微秒时高电平时通常k等于17或18
            data.append(1)      #在数据列表后面添加一位新的二进制数据“1”
 
        j += 1
 
    print("sensor is working.")
    print ('初始数据高低电平:\n',data)    #输出初始数据高低电平
    print ('参数k的列表内容:\n',kk)      #输出高电平结束后的k值
    
    m = np.logspace(7,0,8,base=2,dtype=int) #logspace()函数用于创建一个于等比数列的数组
    #即[128 64 32 16 8 4 2 1],8位二进制数各位的权值
    data_array = np.array(data) #将data列表转换为数组

    #dot()函数对于两个一维的数组,计算的是这两个数组对应下标元素的乘积和(数学上称之为内积)
    humidity = m.dot(data_array[0:8])           #用前8位二进制数据计算湿度的十进制值
    humidity_point = m.dot(data_array[8:16])
    temperature = m.dot(data_array[16:24])
    temperature_point = m.dot(data_array[24:32])
    check = m.dot(data_array[32:40])
    
    print (humidity,humidity_point,temperature,temperature_point,check)
    
    tmp = humidity + humidity_point + temperature + temperature_point
    #十进制的数据相加
 
    if check == tmp:    #数据校验,相等则输出
        return humidity, temperature
    else:               #错误输出错误信息
        return False
 
def main():
    print ("Raspberry Pi DHT11 Temperature test program\n")
    time.sleep(1)           #通电后前一秒状态不稳定,时延一秒
    while True:
        result = read_dht11_dat()
        if result:
            humidity, temperature = result
            print ("humidity: %s %%,  Temperature: %s  ℃" % \
                  (humidity, temperature))
            print ('\n') 
            time.sleep(1)

        if result == False:
            print ("Data are wrong,skip\n")
            time.sleep(1)
            
def destroy():
    GPIO.cleanup()

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        destroy() 
        
     

vsCode 中安装 Remote-SSH 插件,可以直接在 vsCode 中远程连接树莓派,方便进行代码编写和运行。

image-20211108204145555

可以看到,已经可以正常读取温湿度数据。

四、阿里云IoT平台

设备接入

1、准备工作

1.1 注册阿里云账号

使用支付宝或手机号开通阿里云账号,并完成实名认证。

1.2 开通物联网平台服务

打开产品与服务,找到物联网平台并进入,然后点击【立即开通】即可。

image-20211109101844007

开通物联网平台,进入管理控制台可以看到系统提供了一个免费的公共实例,等待公共实例完成开通(大概需要1~2分钟)

image-20211109103028373

2、产品配置

2.1 创建产品
image-20211109103605558
2.2 功能定义,为产品物模型添加自定义功能属性
属性名标识符数据类型取值范围
温度temperaturefloat (单精度浮点型)-50 ~ 100
湿度humidityfloat (单精度浮点型)0 ~ 100
当前时间currentTimetext (字符串)数据长度:40

image-20211109104327962

注意:物模型需要点击发布上线后才能生效使用。

打开物模型通讯Topic,可以看到属性上报 功能,后续传感器上报数据即是通过此 topic 上传。

image-20211109105115442

2.3 设备注册

新建一个设备,打开设备信息获取身份三元组数据,用于后续传感器上报数据的验证。

image-20211109104944893

3、程序开发

3.1 创建文件夹并安装依赖包
mkdir sensor
cd sendor
pip install paho-mqtt
3.2 创建程序文件,添加内容
touch thermometer.py

程序代码(注意修改里面的参数):

# -*- coding: utf-8 -*-
import paho.mqtt.client as mqtt
import time
import hashlib
import hmac
import random
import json

options = {
    'productKey':'你的productKey',
    'deviceName':'你的deviceName',
    'deviceSecret':'你的deviceSecret',
    'regionId':'cn-shanghai'
}

HOST = options['productKey'] + '.iot-as-mqtt.'+options['regionId']+'.aliyuncs.com'
PORT = 1883 
PUB_TOPIC = "/sys/" + options['productKey'] + "/" + options['deviceName'] + "/thing/event/property/post";


# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    # client.subscribe("the/topic")

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
    print(msg.topic+" "+str(msg.payload))

def hmacsha1(key, msg):
    return hmac.new(key.encode(), msg.encode(), hashlib.sha1).hexdigest()

def getAliyunIoTClient():
	timestamp = str(int(time.time()))
	CLIENT_ID = "paho.py|securemode=3,signmethod=hmacsha1,timestamp="+timestamp+"|"
	CONTENT_STR_FORMAT = "clientIdpaho.pydeviceName"+options['deviceName']+"productKey"+options['productKey']+"timestamp"+timestamp
	# set username/password.
	USER_NAME = options['deviceName']+"&"+options['productKey']
	PWD = hmacsha1(options['deviceSecret'],CONTENT_STR_FORMAT)
	client = mqtt.Client(client_id=CLIENT_ID, clean_session=False)
	client.username_pw_set(USER_NAME, PWD)
	return client

if __name__ == '__main__':

	client = getAliyunIoTClient()
	client.on_connect = on_connect
	client.on_message = on_message
	
	client.connect(HOST, 1883, 300)
    
	payload_json = {
		'id': int(time.time()),
		'params': {
			'temperature': random.randint(20, 30),
            'humidity': random.randint(40, 50)
		},
	    'method': "thing.event.property.post"
	}
	print('send data to iot server: ' + str(payload_json))

	client.publish(PUB_TOPIC,payload=str(payload_json),qos=1)
	client.loop_forever()

在控制台中执行下程序,可以看到

image-20211109113203560

此时打开阿里云物联网平台,选择设备可以看到设备已经被激活,物模型上已经有刚才上传的数据记录。

image-20211109154101842

获取传感器数据

当然上述例子中生成的只是一些随机数且只能运行一次,下面需要对程序改动一下使其能够获取树莓派温湿度传感器数据。

创建读取传感器数据的文件:

touch  dht_sensor.py	

在文件中填充以下内容:

#!/usr/bin/python3
#
import RPi.GPIO as GPIO
import time

tmp=[]      # 用来存放读取到的数据

data = 4   # DHT11的data引脚连接到的树莓派的GPIO引脚,使用BCM编号
a,b=0,0

def __delayMicrosecond(t):    # 微秒级延时函数
    start,end=0,0           # 声明变量
    start=time.time()       # 记录开始时间
    t=(t-3)/1000000     # 将输入t的单位转换为秒,-3是时间补偿
    while end-start<t:  # 循环至时间差值大于或等于设定值时
        end=time.time()     # 记录结束时间

def __set_gpio_and_read():
    GPIO.setup(data, GPIO.OUT)  # 设置GPIO口为输出模式
    GPIO.output(data,GPIO.HIGH) # 设置GPIO输出高电平
    __delayMicrosecond(10*1000)   # 延时10毫秒
    GPIO.output(data,GPIO.LOW)  # 设置GPIO输出低电平
    __delayMicrosecond(25*1000)   # 延时25毫秒      
    GPIO.output(data,GPIO.HIGH) # 设置GPIO输出高电平
    GPIO.setup(data, GPIO.IN)   # 设置GPIO口为输入模式

    a=time.time()           # 记录循环开始时间
    while GPIO.input(data): # 一直循环至输入为低电平
        b=time.time()       # 记录结束时间
        if (b-a)>0.1:       # 判断循环时间是否超过0.1秒,避免程序进入死循环卡死
            break           # 跳出循环
        
    a=time.time()
    while GPIO.input(data)==0:  # 一直循环至输入为高电平
        b=time.time()
        if (b-a)>0.1:
            break
                
    a=time.time()
    while GPIO.input(data): # 一直循环至输入为低电平
        b=time.time()
        if (b-a)>=0.1:
            break   
            
    for i in range(40):         # 循环40次,接收温湿度数据
        a=time.time()
        while GPIO.input(data)==0:  #一直循环至输入为高电平
            b=time.time()
            if (b-a)>0.1:
                break

        __delayMicrosecond(28)    # 延时28微秒
            
        if GPIO.input(data):    # 超过28微秒后判断是否还处于高电平
            tmp.append(1)       # 记录接收到的bit为1
                
            a=time.time()
            while GPIO.input(data): # 一直循环至输入为低电平
                b=time.time()
                if (b-a)>0.1:
                    break
        else:
            tmp.append(0)       # 记录接收到的bit为0
            
def get_sensor():
    GPIO.setmode(GPIO.BCM)      # 设置为BCM编号模式
    GPIO.setwarnings(False)
    del tmp[0:]                 # 删除列表
    time.sleep(1)               # 延时1秒
    
    # 配置GPIO信息及读取数据
    __set_gpio_and_read()
  
    humidity_bit=tmp[0:8]       # 分隔列表,第0到7位是湿度整数数据
    humidity_point_bit=tmp[8:16]# 湿度小数
    temperature_bit=tmp[16:24]  # 温度整数
    temperature_point_bit=tmp[24:32]    # 温度小数
    check_bit=tmp[32:40]        # 校验数据
 
    humidity_int=0
    humidity_point=0
    temperature_int=0
    temperature_point=0
    check=0
#  
    for i in range(8):          # 二进制转换为十进制( 2 ** n  表示2的n次方)
        humidity_int+=humidity_bit[i] * 2 ** (7-i)
        humidity_point+=humidity_point_bit[i] * 2 ** (7-i)
        temperature_int+=temperature_bit[i] * 2 ** (7-i)
        temperature_point+=temperature_point_bit[i] * 2 ** (7-i)
        check+=check_bit[i] * 2 ** (7-i)
  
    humidity=humidity_int+humidity_point/10
    temperature=temperature_int+temperature_point/10 - 7
    currentTime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
  
    check_tmp=humidity_int+humidity_point+temperature_int+temperature_point
  
    if check==check_tmp and temperature!=0 and temperature!=0:  # 判断数据是否正常
        print("温度: ", temperature, "℃\n湿度: ", humidity, "%\n当前时间: ", currentTime)# 打印温湿度数据
        print("--------------")
        GPIO.cleanup()
        return temperature,humidity,currentTime
    else:
        GPIO.cleanup()
        print("error")
        return 0,0,currentTime

修改 thermometer.py 文件

# -*- coding: utf-8 -*-
import paho.mqtt.client as mqtt
import time
import hashlib
import hmac
import random
import json
import dht_sensor as dht

options = {
    'productKey':'你的productKey',
    'deviceName':'你的deviceName',
    'deviceSecret':'你的deviceSecret',
    'regionId':'cn-shanghai'
}

HOST = options['productKey'] + '.iot-as-mqtt.'+options['regionId']+'.aliyuncs.com'
PORT = 1883 
PUB_TOPIC = "/sys/" + options['productKey'] + "/" + options['deviceName'] + "/thing/event/property/post";


# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    print("Connected with result code "+str(rc))
    # client.subscribe("the/topic")

# The callback for when a PUBLISH message is received from the server.
def on_message(client, userdata, msg):
    print(msg.topic+" "+str(msg.payload))

def hmacsha1(key, msg):
    return hmac.new(key.encode(), msg.encode(), hashlib.sha1).hexdigest()

def getAliyunIoTClient():
	timestamp = str(int(time.time()))
	CLIENT_ID = "paho.py|securemode=3,signmethod=hmacsha1,timestamp="+timestamp+"|"
	CONTENT_STR_FORMAT = "clientIdpaho.pydeviceName"+options['deviceName']+"productKey"+options['productKey']+"timestamp"+timestamp
	# set username/password.
	USER_NAME = options['deviceName']+"&"+options['productKey']
	PWD = hmacsha1(options['deviceSecret'],CONTENT_STR_FORMAT)
	client = mqtt.Client(client_id=CLIENT_ID, clean_session=False)
	client.username_pw_set(USER_NAME, PWD)
	return client


if __name__ == '__main__':
        while True:
            client = getAliyunIoTClient()
            client.on_connect = on_connect
            client.on_message = on_message
            client.connect(HOST, 1883, 300)
            temp,hum,currentTime = dht.get_sensor()
            
            payload_json = {
                'id': int(time.time()),
                    'params': {
                        'temperature': temp,
                        'humidity': hum,
                        'currentTime': currentTime
                    },
                'method': "thing.event.property.post"
            }
            client.publish(PUB_TOPIC,payload=str(payload_json),qos=1)
            time.sleep(3)
        client.loop_forever()

在控制台中运行一下程序,可以看到每隔4秒数据就会上报一次。

image-20211109155935054

打开阿里云物联网平台可以看到数据上报也是正常的

image-20211109160136568

五、更好玩的方式

上述例子中我们描述的是一个简单的 IoT 设备数据到物联网平台的数据采集流程,要想实时的获取办公室的温湿度数据,还得去网页上查看,或者得写一个专门从物联网平台获取转发数据的程序,这种就显得比较麻烦了,有没有什么办法把这些数据直接推送给我们呢,比如企业微信、钉钉等,这里我们以企业微信为例,实现这种好玩的方式。

打开企业微信,选择群聊新建一个群机器人。

image-20211110104053405

可以看到群机器人中提供了一个Webhook的功能(钉钉上同样也提供 webhook 功能),点击 Webhook 地址可以进入到机器人配置页面。

image-20211110104426976

我们复制上图中的示例脚本在服务器中执行下:

curl 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=693axxx6-7aoc-4bc4-97a0-0ec2sifa5aaa' \
   -H 'Content-Type: application/json' \
   -d '
   {
        "msgtype": "text",
        "text": {
            "content": "hello world"
        }
   }'

执行结果:

image-20211110110606928 image-20211110110744586

可以看到企业微信中也正常推送 hello world 消息,由此可知,我们可以通过调用企业微信中群机器人的 Webhook 地址,并传入期望的参数,就可以在企业微信中获取我们想要的数据。

下面我们仍然使用 python 编码实现(注意修改 webhook 地址):

touch weixin.py
#-*- coding: utf-8 -*-

import os
import logging
import requests
import dht_sensor as dht
import time

#Webhook地址
headers = {"Content-Type": "application/json"}
curl='填写群机器人的 Webhook地址'


while True:
        # 获取温湿度数据
        temperature,humidity,currentTime = dht.get_sensor()
        if humidity == 0:
            	# 异常数据,等待下一次循环
                continue
        # 上传至企业微信的数据
        data = {
                "msgtype": "markdown",
                "markdown": {
                        "content": "### -- 办公室当前温湿度数据 --\n\n  >温度:  <font color=\"info\">"+str(temperature)
                        +" ℃</font>\n\n  >湿度:  <font color=\"info\">"+str(humidity)
                        +" %</font>\n\n  >时间:  <font color=\"info\">"+str(currentTime)+"</font>"
                }
        }
        # 调用 webhook 地址
        r = requests.post(url = curl,headers = headers,json = data)
        logging.basicConfig(level = logging.DEBUG,format = '%(asctime)s, %(filename)s, %(levelname)s, %(message)s',
                datefmt = '%a, %d %b %Y %H:%M:%S',filename = os.path.join('/home/pi/Desktop/sensor','weixin.log'),filemode = 'a')
        subject="办公室当前温湿度数据"
        logging.info('subject:' + subject + ';message: 温度:' + str(temperature)+'℃,湿度:'+str(humidity) + "%")
        time.sleep(20)

这里设置的是间隔20秒左右上报一次,运行程序:

image-20211110152336535

打开企业微信即可看到实时采集上来的办公室温湿度数据:

image-20211110152644348

附录

1、DHT11 温湿度传感器原理.pdf

2、阿里云物联网平台文档与工具

3、树莓派从DHT11温湿度传感器读取数据

4、树莓派电脑控制温湿度传感器