树莓派3B和攀藤PMS5003ST


更新日期

2025.06.30
使用树莓派5,弃用mysql,使用sqlite

数据仅供学习

使用RS232USB转串口线连接攀藤,参照文档,接VCC,GND和TXD即可,PIN3可设置休眠。
系统raspberry pi os,数据库sqlite,后端php-fpm,网页服务nginx
命令默认使用root账号运行,或者sudo
程序流程定期执行saveToSql.py保存数据到数据库
index.html使用getFromSql.php读取数据并展示
getFromUSB.py命令行获取数据用
sendToHomeassistant.sh获取一个数据给Homeassistant

1
2
3
apt install sqlite3 nginx php8.2-sqlite3 php8.2-common php8.2-cli php8.2-sqlite3 php8.2-curl
systemctl enable nginx
systemctl start nginx

修改nginx配置

编辑/etc/nginx/sites-available/default,没出现的行不需要修改
注意fastcgi_pass这行的php版本号可能需要修改

1
2
3
4
5
6
7
8
9
10
root /srv/http;
index index.php index.html index.htm index.nginx-debian.html;
location ~ \.php$ {
include snippets/fastcgi-php.conf;

# With php-fpm (or other unix sockets):
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
# With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
}

重新载入nginx配置

1
systemctl reload nginx

新建数据库

输入sqlite3进去sqlite

1
2
3
4
5
6
.open /srv/http/pm25/pm25.db
CREATE TABLE pm (
data TEXT NOT NULL,
time DATETIME NOT NULL DEFAULT(datetime(CURRENT_TIMESTAMP,'localtime'))
);
.quit

使用python读取数据,getFromUSB.py,在命令行直接显示数据,网页的话不需要这个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#encoding=utf-8
import os
import serial
import time
from struct import *

ser = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=2.0)

def read_pm_line(_port):
rv = b''
while True:
ch1 = _port.read()
if ch1 == b'\x42':
ch2 = _port.read()
if ch2 == b'\x4d':
rv += ch1 + ch2
rv += _port.read(38)
return rv

def main():
# conn = sqlite3.connect('pm25.db')
# c = conn.cursor()
recv = read_pm_line(ser)

tmp = recv[4:36]
datas = unpack('>hhhhhhhhhhhhhhhh', tmp)
print('Plantower PMS5003ST,Updated:',time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
print('PM1.0(CF=1): {}ug/m3\n'
'PM2.5(CF=1): {}ug/m3\n'
'PM10 (CF=1): {}ug/m3\n'
'PM1.0 (STD): {}ug/m3\n'
'PM2.5 (STD): {}ug/m3\n'
'PM10 (STD): {}ug/m3\n'
'>0.3um : {}/0.1L\n'
'>0.5um : {}/0.1L\n'
'>1.0um : {}/0.1L\n'
'>2.5um : {}/0.1L\n'
'>5.0um : {}/0.1L\n'
'>10um : {}/0.1L\n'
'HCHO : {}mg/m3\n'
'Temperature: {}C\n'
'Humidity : {}%'.format(datas[0], datas[1], datas[2],
datas[3], datas[4], datas[5],
datas[6], datas[7], datas[8],
datas[9], datas[10], datas[11],
datas[12]/1000.0, datas[13]/10.0, datas[14]/10.0))
ser.flushInput()
time.sleep(0.1)

if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
if ser != None:
ser.close()

4.网站主页,单击当前数值显示完整数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>空气质量</title>
<style>
html,body{
margin:0px 0px 0px 0px;
padding:0px 0px 0px 0px;
height:100%;
}
#showLayout{
width:100%;
height:100%;
}
#showLayout p:nth-child(1){
background:#e7f7ed;
}
#showLayout p:nth-child(2){
background:#f9f5e3;
}
#showLayout p:nth-child(3){
background:#fbefe3;
}
#showLayout p:nth-child(4){
background:#fbe2e2;
}
#showLayout p:nth-child(5){
background:#f4e3f5;
}
#showLayout p:nth-child(6){
background:#ede2ed;
}
#showLayout p{
margin:0px 0px 0px 0px;
padding:10px 10px 10px 10px;
width:5%;
height:16.67%;
}
#detailLayout{
display:none;

}#detailLayout p{
margin:0px 0px 0px 0px;
padding:0px 0px 0px 0px;
}
#dataLayout{
display:none;
}
p{
font-size:40pt;
}
</style>
<script type="text/javascript" src="jquery-3.7.1.min.js"></script>
</head>
<body>
<div id="showLayout">
<p></p><p></p><p></p><p></p><p></p><p></p>
</div>
<div id="detailLayout">
</div>
<div id="dataLayout">
<div id="data">
</div>
<div id="date">
</div>
</div>
</body>
</html>
<script type="text/javascript" language="JavaScript">
function getData(){
$.ajax({
url: "getFromSql.php",
dataType: "json",
async: false,
type: "POST",
success: function (data){
$('#data').html(data.pm);
$('#date').html(data.date);
}
});
}
$(document).ready(function(){
getData();
var number = $('#data').html();
//number = number.substring(1,number.length-1);
number = number.split(',');
var backgroundColor = "white";
var fontColor = "black";
var airQuality = "无";
var index = 0;
if (number[4] >= 0 && number[4] <= 12){
fontColor = "#acddc6";
airQuality = "优";
index = 0;
}
if (number[4] > 12 && number[4] <= 35){
fontColor = "#e0b902";
airQuality = "良";
index = 1;
}
if (number[4] > 35 && number[4] <= 55){
fontColor = "#f08008";
airQuality = "轻度污染";
index = 2;
}
if (number[4] > 55 && number[4] <= 150){
fontColor = "#db2d01";
airQuality = "中度污染";
index = 3;
}
if (number[4] > 150 && number[4] <= 250){
fontColor = "#c870cd";
airQuality = "重度污染";
index = 4;
}
if (number[4] > 250 && number[4] <= 1000){
fontColor = "#8f6091";
airQuality = "严重污染";
index = 5;
}
$('#showLayout p:eq(' + index + ')').html('<span>' + airQuality + ':' + number[4] + 'μg/m³</span><br/><span>温度:' + number[13] + '℃ 湿度:' + number[14] + '%</span>');
$('#showLayout p:eq(' + index + ')').css("color",fontColor);
$('#showLayout p:eq(' + index + ')').css("text-align","center");
$('#showLayout p:eq(' + index + ')').css('width','100%');
var fontSize = $(window).height() / 16;
$('p').css('font-size',fontSize);
$('#showLayout p:eq(' + index + ')').click(function(){
if($('#detailLayout').css('display') == 'none'){
$('#detailLayout').show();
$('#detailLayout').html('<p>数据更新时间:'+$('#date').html()+'</p><p>数据测量位置:广东省广州市</p><p>标准颗粒物质量浓度(CF=1)</p><p>PM 1.0: '+number[0]+' μg/m³</p><p>PM 2.5: '+number[1]+' μg/m³</p><p>PM 10 : '+number[2]+' μg/m³</p><p>大气环境下颗粒物质量浓度</p><p>PM 1.0: '+number[3]+' μg/m³</p><p>PM 2.5: '+number[4]+' μg/m³</p><p>PM 10 : '+number[5]+' μg/m³</p><p>0.1升空气中直径大于某值的颗粒物个数</p><p>>0.3μm: '+number[6]+'/0.1L</p><p>>0.5μm: '+number[7]+'/0.1L</p><p>>1.0μm: '+number[8]+'/0.1L</p><p>>2.5μm: '+number[9]+'/0.1L</p><p>>5.0μm: '+number[10]+'/0.1L</p><p>>10 μm: '+number[11]+'/0.1L</p><p>甲醛: '+number[12]+'mg/m³</p><p>温度: '+number[13]+'℃</p><p>湿度: '+number[14]+'%</p>');
}
else{
$('#detailLayout').hide();
$('#detailLayout').html('');
}
});
});
</script>

getFromSql.php,给index.html获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$dsn = 'sqlite:/srv/http/pm25/pm25.db';
try {
$db = new PDO($dsn);
} catch(PDOException $e) {
die('Could not connect to the database:' . $e);
}
//$db->query('set names utf8'); // SQLite 不需要设置字符集
$sql = "select * from pm order by time desc limit 1";
$obj = $db->prepare($sql);
$obj->execute();
$arr = $obj->fetchAll(PDO::FETCH_ASSOC);
$data = array("pm"=>$arr[0]['data'],"date"=>$arr[0]['time']);
echo json_encode($data);
//print_r($arr[0][time]);
?>

saveToSql.py,保存数据到sqlite数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# encoding=utf-8
import os
import serial
import time
import json
import sqlite3
from struct import *

# pip install requests

ser = serial.Serial("/dev/ttyUSB0", baudrate=9600, timeout=2.0)


def read_pm_line(_port):
rv = b''
while True:
ch1 = _port.read()
if ch1 == b'\x42':
ch2 = _port.read()
if ch2 == b'\x4d':
rv += ch1 + ch2
rv += _port.read(38)
return rv

def writefile(filereadlines):
#write file
newfile = open('/srv/http/pm25/data.json', mode='w', encoding='UTF-8')
newfile.writelines(filereadlines)
newfile.close()

def save_to_sqlite(data):
"""直接保存数据到SQLite数据库"""
dbfile = '/srv/http/pm25/pm25.db'
try:
# 连接到SQLite数据库
conn = sqlite3.connect(dbfile)
cursor = conn.cursor()

# 插入数据
sql = "INSERT INTO pm(data) VALUES(?)"
cursor.execute(sql, (data,))

# 提交事务
conn.commit()
print(f"数据已保存到数据库: {data}")

except sqlite3.Error as e:
print(f"数据库操作失败: {e}")
except Exception as e:
print(f"保存数据时发生错误: {e}")
finally:
if conn:
conn.close()

def main():
recv = read_pm_line(ser)

tmp = recv[4:36]
datas = unpack('>hhhhhhhhhhhhhhhh', tmp)
sendData = datas[0], datas[1], datas[2], datas[3], datas[4], datas[5], datas[6], datas[
7], datas[8], datas[9], datas[10], datas[11], datas[12]/1000.0, datas[13]/10.0, datas[14]/10.0
sendData = str(tuple(sendData))
tmp = sendData.split(' ')
sendData = ''.join(tmp)

# 直接保存到SQLite数据库,而不是通过HTTP请求
save_to_sqlite(sendData[1:-1])

ser.flushInput()
time.sleep(0.1)

outputData = {'temperature':datas[13]/10,'humidity':datas[14]/10,'pm25':datas[4]}

writefile(json.dumps(outputData))


if __name__ == '__main__':
try:
main()
except KeyboardInterrupt:
if ser != None:
ser.close()

每10分钟保存数据到sqlite

1
crontab -e

添加一行,

1
*/10 * * * * /usr/bin/python /srv/http/pm25/saveToSql.py