Chapter 12 · 树莓派与嵌入式 Linux

综合实战:家庭自动化中枢

把前 11 章的技艺熔于一炉:传感器采集 → MQTT 通信 → 数据入库 → Web 面板可视化 → 常驻自启。从零搭一个真正跑得起来、断电重启也能自愈的家庭自动化中枢。

课程进度100%

关键概念

中枢(Hub)架构
家庭自动化中枢是各种传感器、执行器、界面之间的协调中心。树莓派扮演这个角色:本地采集数据、经 MQTT 汇聚、存储、展示、执行自动化规则。所有设备围绕中枢通信,而非彼此直连——这就是我们前几章逐步搭建的能力总成。
采集层 / 通信层 / 存储层 / 展示层
典型 IoT 系统分四层:采集层(传感器读数,第 5 章)、通信层(MQTT 传输,第 11 章)、存储层(数据入库)、展示层(Web 面板可视化)。分层解耦让系统清晰、可独立替换升级。本章把四层串成一条完整数据流。
SQLite 时序存储
SQLite 是零配置的嵌入式数据库(单文件),Python 内置 sqlite3 模块直接用,无需装服务器。适合树莓派存传感器时序数据(时间戳+读数)。数据量大或高频写入时可换 InfluxDB 等时序库,但入门 SQLite 足够,且省资源。
Flask 轻量 Web 面板
Flask 是 Python 极简 Web 框架,几十行就能起一个网页服务。用它读数据库、渲染实时数据页面,手机浏览器打开就能看家里温湿度、控制设备。轻量到树莓派毫无压力,是自制 IoT 面板的常用选择。
自动化规则(Rule Engine)
"当温度 > 28°C 时开风扇"这类条件→动作的逻辑就是自动化规则。中枢订阅传感器数据,匹配规则后发布控制指令(经 MQTT 或直接操作 GPIO)。这是自动化的灵魂——让系统自主响应环境,无需人干预。
优雅退出与信号处理
常驻程序被 systemd/Docker 停止时会收到 SIGTERM 信号。捕获它做清理(断开 MQTT、关闭数据库、断电 GPIO),避免数据损坏或硬件异常。Python 用 signal 模块注册处理函数。生产级服务必须优雅退出,不能一杀了之。
配置与代码分离
把 Broker 地址、密码、阈值、GPIO 引脚等写进配置文件(.env / config.yaml)或环境变量,而非硬编码在代码里。换环境改配置即可,密码不进 Git。这是可维护、可移植部署的基本功。
部署 Checklist
上线前的检查清单:静态 IP/主机名、SSH 密钥、防火墙、服务自启、日志可查、崩溃自愈、数据持久化、备份、时间同步(NTP)、资源监控。逐项确认,才能放心让设备无人值守长期运行。本章末尾给出完整清单。

系统总架构

把前 11 章的能力组装成一个完整的数据流水线——从传感器到手机屏幕:

家庭自动化中枢 · 完整架构(前 11 章能力总成) ═══════════════════════════════════════════════════════════════════ ① 采集层(第5章) ② 通信层(第11章) ③ 存储+规则层 ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ BMP280 (I2C) │─读数──► │ │ │ collector.py │ │ DS18B20(1wire)│ │ Mosquitto │──订阅─►│ ├ 入库 SQLite│ │ 光敏(MCP3008) │─publish─►│ Broker │ │ └ 规则引擎 │ └──────────────┘ │ home/# │ └──────┬───────┘ │ │◄──控制指令──────┘ ④ 执行层(第4/6章) │ │ (温度高→开风扇) ┌──────────────┐ └──────┬───────┘ │ 继电器/风扇 │◄─订阅 cmd───────┘ ⑤ 展示层 │ 舵机/LED │ ┌──────────────┐ └──────────────┘ │ Flask Web │ │ 读 SQLite │ ⑥ 常驻层(第9/10章): systemd/Docker 托管 ───► │ 手机浏览器看 │ 开机自启 + 崩溃自愈 + journalctl 日志 └──────────────┘ 数据流:传感器 →MQTT→ 中枢入库+判规则 →MQTT→ 执行器;Web 读库展示

第一步:采集并发布(采集层)

整合第 5、11 章。读传感器,通过 MQTT 发布,配置与代码分离:

# sensor_node.py — 采集传感器并发布到 MQTT
import os, json, time, signal, sys
import paho.mqtt.client as mqtt
import board, adafruit_bmp280

# 配置从环境变量读(配置与代码分离)
BROKER = os.getenv("MQTT_BROKER", "localhost")
USER   = os.getenv("MQTT_USER", "piuser")
PASSWD = os.getenv("MQTT_PASS", "")

# 初始化传感器
i2c = board.I2C()
sensor = adafruit_bmp280.Adafruit_BMP280_I2C(i2c, address=0x76)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="sensor-node")
client.username_pw_set(USER, PASSWD)
client.will_set("home/livingroom/status", "offline", retain=True)
client.connect(BROKER, 1883, 60)
client.loop_start()
client.publish("home/livingroom/status", "online", retain=True)

# 优雅退出:收到 SIGTERM 时清理
def shutdown(sig, frame):
    client.publish("home/livingroom/status", "offline", retain=True)
    client.loop_stop(); client.disconnect()
    sys.exit(0)
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)

while True:
    payload = json.dumps({
        "temp": round(sensor.temperature, 1),
        "pressure": round(sensor.pressure, 1),
        "ts": int(time.time()),
    })
    client.publish("home/livingroom/temp", payload, qos=1, retain=True)
    time.sleep(10)

第二步:中枢入库 + 规则引擎(存储+规则层)

中枢订阅数据,写入 SQLite,并执行自动化规则(超温发控制指令):

# collector.py — 订阅、入库、跑规则
import sqlite3, json, os
import paho.mqtt.client as mqtt

DB = "/home/pi/hub/data.db"
FAN_THRESHOLD = float(os.getenv("FAN_TEMP", "28"))

# 建表(若不存在)
conn = sqlite3.connect(DB, check_same_thread=False)
conn.execute("""CREATE TABLE IF NOT EXISTS readings(
    ts INTEGER, room TEXT, temp REAL, pressure REAL)""")
conn.commit()

def on_connect(c, u, f, rc, p):
    c.subscribe("home/+/temp", qos=1)

def on_message(c, u, msg):
    d = json.loads(msg.payload)
    room = msg.topic.split("/")[1]
    # 入库
    conn.execute("INSERT INTO readings VALUES (?,?,?,?)",
                 (d["ts"], room, d["temp"], d.get("pressure")))
    conn.commit()
    # 规则引擎:超温 → 发开风扇指令
    if d["temp"] > FAN_THRESHOLD:
        c.publish(f"home/{room}/cmd/fan", "ON", qos=1)
        print(f"{room} {d['temp']}°C 超阈值,开风扇")
    else:
        c.publish(f"home/{room}/cmd/fan", "OFF", qos=1)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="hub-collector")
client.username_pw_set(os.getenv("MQTT_USER"), os.getenv("MQTT_PASS"))
client.on_connect = on_connect
client.on_message = on_message
client.connect(os.getenv("MQTT_BROKER", "localhost"), 1883, 60)
client.loop_forever()

执行层节点(第 4/6 章)订阅 home/+/cmd/fan,收到 ON 就用 gpiozero 打开继电器/风扇,形成闭环。

第三步:Web 面板(展示层)

用 Flask 读数据库,手机浏览器看实时数据:

# web.py — Flask 实时数据面板
from flask import Flask, jsonify, render_template_string
import sqlite3

app = Flask(__name__)
DB = "/home/pi/hub/data.db"

PAGE = """<!DOCTYPE html><html><head><meta charset="utf-8">
<title>家庭中枢</title><meta http-equiv="refresh" content="10"></head>
<body style="font-family:sans-serif;padding:2rem">
<h1>🏠 家庭自动化中枢</h1>
<p>客厅温度:<b>{{temp}}°C</b> 气压:{{pressure}} hPa</p>
<p>更新时间:{{ts}}</p></body></html>"""

def latest():
    conn = sqlite3.connect(DB)
    row = conn.execute(
        "SELECT temp,pressure,ts FROM readings ORDER BY ts DESC LIMIT 1"
    ).fetchone()
    conn.close()
    return row

@app.route("/")
def index():
    t, p, ts = latest() or ("--", "--", "--")
    return render_template_string(PAGE, temp=t, pressure=p, ts=ts)

@app.route("/api/latest")
def api():          # 给前端图表/App 用的 JSON 接口
    t, p, ts = latest() or (None,) * 3
    return jsonify(temp=t, pressure=p, ts=ts)

if __name__ == "__main__":
    # 0.0.0.0 让局域网可访问
    app.run(host="0.0.0.0", port=8000)

第四步:常驻部署(常驻层)

用第 9 章 systemd 把三个进程都托管为自启服务。以中枢为例:

# /etc/systemd/system/hub-collector.service
[Unit]
Description=家庭中枢数据采集与规则引擎
After=network-online.target mosquitto.service
Wants=network-online.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/hub
EnvironmentFile=/home/pi/hub/.env       # 配置分离
Environment=PYTHONUNBUFFERED=1
ExecStart=/home/pi/hub/env/bin/python /home/pi/hub/collector.py
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
# 三个服务全部启用自启(sensor_node / collector / web)
sudo systemctl daemon-reload
sudo systemctl enable --now hub-collector hub-web sensor-node

# 查看整体运行状态
systemctl status hub-collector hub-web sensor-node

# 实时看中枢日志
journalctl -u hub-collector -f

# 手机浏览器打开 http://raspberrypi.local:8000 看面板
或者全部 Docker Compose 化

更优雅的方式是把 Mosquitto、collector、web 全写进一份 compose.yaml(第 10 章),docker compose up -d 一键拉起、统一管理、换机秒迁移。硬件访问节点(sensor_node)用 --device 映射 I2C/GPIO。两种方式都行:systemd 更贴近系统、Docker 更易迁移,按喜好选。

上线部署 Checklist

让中枢无人值守长期稳定运行,上线前逐项确认:

网络与访问

  • ✅ 设静态 IP 或路由器 MAC 绑定(第 8 章)
  • ✅ SSH 密钥登录,禁用密码
  • ✅ ufw 防火墙只放行 22/1883/8000
  • ✅ 异地访问用 Tailscale,不裸奔公网
  • ✅ NTP 时间同步开启(时间戳准确)

可靠性

  • ✅ 所有服务 systemd enable 自启
  • ✅ Restart=always 崩溃自愈
  • ✅ 优雅退出处理 SIGTERM
  • ✅ MQTT 遗嘱消息标记离线

数据与存储

  • ✅ SQLite/数据卷放外接 USB SSD 护 SD 卡
  • ✅ 定期备份数据库(cron/timer)
  • ✅ 日志轮转,防塞满磁盘
  • ✅ 高频数据边缘聚合再存/上报

安全与运维

  • ✅ 密码/密钥进 .env,不入 Git
  • ✅ MQTT 认证开启,公网走 TLS
  • ✅ 定期 apt 升级 + docker prune
  • ✅ 监控 CPU 温度/内存(防降频/OOM)
  • ✅ 加散热片/风扇(长时间满载)
长期运行的隐形杀手

无人值守设备最怕三件事:① SD 卡损耗——频繁写入几个月就坏,务必把数据库/日志迁到 USB SSD,或开 log2ram;② 磁盘塞满——日志和数据无限增长最终撑爆,配日志轮转和数据清理;③ 过热降频——夏天或满载时 CPU 超 80°C 掉速甚至重启,加散热并用 vcgencmd measure_temp 监控。这三点做不好,Demo 能跑但撑不过一个季度。

全书总结:你已掌握从零到中枢的完整链路

十二章走下来,你把树莓派从一块陌生的电路板,变成了能采集、通信、存储、展示、自动响应的家庭自动化中枢。回顾这条路:认识单板计算机与 GPIO(1、4 章)→ 烧录系统并用 Linux 命令驾驭它(2、3 章)→ 接 I2C/SPI/1-Wire 传感器感知世界、用 PWM 驱动电机、摄像头视觉(5、6、7 章)→ SSH 远程掌控、systemd 常驻自启、Docker 容器化部署(8、9、10 章)→ MQTT 联网上云,最终综合实战把一切串成一套断电自愈、无人值守的系统(11、12 章)。树莓派的魅力正在于此——它既是学习 Linux 与嵌入式的最佳教具,也是能真正跑在你家角落、日夜工作的生产设备。接下来,把这套架构套用到你自己的点子上:气象站、宠物投喂器、NAS、家庭监控、边缘 AI 推理……硬件与软件的边界,就在你的想象力那里。愿你玩得尽兴。