树莓派-家庭健康监测-空气篇
最后编辑日期: 2023.7.16
1、背景和目标
室内空气中高浓度的二氧化碳会损害人类的认知能力和健康,良好室内空气质量的重要性和价值不言而喻。家庭健康监测-空气篇应运而生。
最后效果总览:
细节:
手机预览:
实现功能:
- 二氧化碳浓度监测
- 空气温湿度监测
- 根据Co2浓度, 指示灯给出提示, 绿色: Co2浓度很低; 蓝色: 良好; 红色: 差。
- 局域网内手机预览结果
1、主要硬件型号
二氧化碳浓度+温湿度
传感器: SCD41- 树莓派4b Raspberry Pi 4B
- WS2812彩色LED
- 0.96寸OLED显示屏
2、软件环境
树莓派: Raspberry Pi OS Lite (64-bit)
开发环境: node.js(v18.16.1)
和 Python 3.9.2
主要是 javascript
和少量的 python
.
node -v
python3 --version
3、软件结构
- 一共3个进程,通过 WebSocket 和 HTTP 通信。
- App作为 WebSocket 服务端,当有客户端连接后,就会定时推送数据
- Web Server 作为 WebSocket 客户端,从App获得数据后存在自己本地,相当于数据中转站,用于数据分发。
遇到的问题:
- LED的控制也想用js控制,但是
node-gyp
编译失败了。 - led.py 也想用 WebSocket 通信, 也失败了😭。
4、代码实现
- 因为serve.js是 WebSocket 的客户端,所以要等App(WebSocket 服务端)上线,有个轮训过程。
- led.py 进程是在获取到传感器数据后才启动的,这里发现过早使用led 会有问题,突然控制不了led灯了。
4.1、serve.js
console.error("开始运行serve...");
const Koa = require('koa');
const cors = require('@koa/cors');
const Router = require("koa-router");
const BodyParser = require('koa-bodyparser');
const Static = require("koa-static");
const WebSocket = require('ws');
const { exec } = require('child_process');
function getSocketServerIsOnline(cb) {
exec("netstat -tln | grep ':8888'", (error, stdout, stderr) => {
if (error) {
cb(false);
return;
}
if (stdout.length === 0) {
cb(false);
return;
}
cb(true);
});
}
let hasStartLed = false;
function startLed() {
console.error("启动 led service...");
if(hasStartLed) return;
hasStartLed = true;
exec("sudo systemctl start led.service", (error, stdout, stderr) => {
if (error) {
// cb(false);
console.error(error);
return;
}
if (stdout.length !== 0) {
console.error(stdout);
return;
}
});
}
let currentData = "";
let co2Data = 0;
function initSocketClient() {
let ws = null;
try {
ws = new WebSocket('ws://localhost:8888');
} catch (error) {
console.error('连接失败:', error);
}
if (!ws) return;
ws.on('open', () => {
console.log('WebSocket connected');
});
let updateTimer = -1;
ws.on('message', (message) => {
currentData = message;
const data = message.toString("utf-8")
co2Data = data.split('&')[0];
if(co2Data > 0) {
startLed()
}
clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
co2Data = 0;
}, 10000);
});
ws.on('close', () => {
console.log('WebSocket closed');
});
}
let checkOnlineTimer = setInterval(() => {
getSocketServerIsOnline(isOnline => {
if (isOnline) {
console.error("8888 is online...");
clearInterval(checkOnlineTimer);
initSocketClient();
}
})
}, 1000);
const app = new Koa();
console.error("创建 koa...");
// 启用CORS
app.use(cors({
origin: '*',
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization']
}));
// 这个koa-bodyparser必须在router之前被注册到app对象上
app.use(BodyParser());
const router = new Router();
router.get('/getSensorData', async ctx => {
try {
ctx.body = currentData
}
catch (error) {
console.log('get_config.error', error)
}
})
router.get('/getCo2Data', async ctx => {
ctx.body = co2Data
console.log('getCo2Data...', co2Data)
})
app.use(Static(__dirname));
app.use(router.routes());
app.listen(8080)
4.2、app.js
const { exec } = require('child_process');
const OledDevice = require("./sensor/oled")
const SCD4XDevice = require('./sensor/scd4x.js');
const WebSocket = require('ws');
const { getIP, getCPUTemp } = require("./device.js")
class App {
constructor() {
this.data = {
co2: 0,
temperature: 0,
humidity: 0,
ip: null,
cpuTemp: null
};
this.oled = new OledDevice()
this.scd4x = new SCD4XDevice()
let hasInit = false;
let timer = -1;
const checkNetOk = (ip)=>{
if(hasInit === false && ip && ip.indexOf("192.168.124.") !== -1) {
hasInit = true;
this.initWebSocketServe();
// 启动 webserver 和 websocket client
exec('node ../serve/serve.js')
this.loopPiData()
this.initSensor();
clearInterval(timer)
}
}
timer = setInterval(() => {
getIP(ip => {
this.data.ip = ip;
checkNetOk(ip)
})
}, 500);
}
oledShowData() {
this.oled.writeString({
size: 2,
x: 1,
y: 1,
text: this.data.co2 + " ppm"
})
this.oled.writeString({
size: 2,
x: 1,
y: 24,
text: this.data.temperature + " 'c"
})
this.oled.writeString({
size: 2,
x: 1,
y: 48,
text: this.data.humidity + " %"
})
}
sendDataBySocket(data) {
if (!this.ws) { return }
this.ws.send(data)
}
sendData() {
let sendDataContext = `${this.data.co2}&${this.data.temperature}&${this.data.humidity}`;
if (this.data.ip && this.data.cpuTemp) {
sendDataContext += `&${this.data.ip}&${this.data.cpuTemp}`
}
this.sendDataBySocket(sendDataContext)
}
loopPiData() {
setInterval(() => {
getCPUTemp(temp => {
this.data.cpuTemp = temp.match(/\d+\.\d+/)[0];
this.sendData()
})
}, 1000);
}
loopSensorData() {
setInterval(() => {
this.scd4x.readSensorData()
.then(async data => {
this.data.co2 = data.co2;
this.data.temperature = data.temperature.toFixed(2);
this.data.humidity = data.humidity.toFixed(2);
this.oledShowData()
this.sendData()
});
}, 5000);
}
initWebSocketServe() {
const wss = new WebSocket.Server({ port: 8888 });
wss.on('connection', (ws) => {
console.log('WebSocket connected');
// ws.on('message', (message) => {
// console.log(`Received message: ${message}`);
// ws.send(`Server received message: ${message}`);
// })
ws.on('close', () => {
this.ws = null;
console.log('WebSocket closed')
})
this.ws = ws;
})
}
async initSensor() {
this.oled.writeString({
size: 2,
x: 1,
y: 1,
text: "scd4x init..."
})
await this.scd4x.initialize();
await this.scd4x.startPeriodicMeasurement();
this.oled.writeString({
size: 2,
x: 1,
y: 1,
text: "scd4x ready..."
})
this.oled.clear();
this.loopSensorData()
}
}
new App()
4.3、led.py
当data大于0才会启动该进程,不然led失去控制当自动运行的时候。
import time
import MyHttp
import MyLed
data = 0
def getData(_data):
global data # 声明data为全局变量
data = int(_data)
def breathLed():
MyLed.set_brightness(20)
MyLed.set_pixel_color([255,0,0])
start_time = time.time()
while (time.time() - start_time) < 5:
MyLed.breathe()
while True:
MyLed.set_brightness(20)
MyHttp.get_request("http://192.168.124.17:8080/getCo2Data", getData)
if(data == 0):
breathLed()
print("等待数据...")
else :
time.sleep(2)
print(data)
if data < 500:
MyLed.set_pixel_color([0,255,0])
elif 500 <= data < 800:
MyLed.set_pixel_color([0,0,255])
elif 800 <= data < 2000:
MyLed.set_pixel_color([255,0,0])
else:
# 异常值
MyLed.set_pixel_color([255,255,0])
5、自动运行
使用守护进程, /etc/systemd/system 目录下:
sudo nano /etc/systemd/system/ web.service
sudo nano /etc/systemd/system/ app.service
sudo nano /etc/systemd/system/ led.service
web.service:
[Unit]
Description=Run my scripts on startup
[Service]
ExecStart=/bin/bash -c "node /home/pi/Documents/home/app.js"
WorkingDirectory=/home/pi/Documents/home
Restart=always
User=pi
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable web.service
sudo systemctl start web.service
资料
raspberry pi
python lib