3.树莓派本地SQLite存储DHT22温湿度数据

一、主要内容

上篇树莓派上传DHT22温湿度数据到腾讯云物联网平台实现了真实数据上传云端,但本地树莓派上有必要存储数据备份,一是便于Openclaw查阅,二是后期用于数据分析。

因为树莓派上跑的是单机、低频次、小数据量 的传感器采集场景,选 SQLite 是性价比最高的选择。核心就一句话:够用、够轻、够简单

为什么选 SQLite

原因 说明
零安装零服务 SQLite 是文件型数据库,Python 标准库自带 sqlite3,不需要像 MySQL/PostgreSQL 那样安装、启动、配置服务。
资源占用极低 树莓派内存和 CPU 有限,SQLite 不需要常驻进程,只有运行 SQL 时才占用资源。
文件即数据库 整个数据库就是一个 .db 文件,备份、迁移、复制都极其简单。
查询能力完整 支持标准 SQL、索引、聚合查询(AVGCOUNT、时间范围查询等),比 CSV/JSON 强大很多。
并发够用 DHT22 通常几分钟才采一条数据,写入频率极低,SQLite 完全能应付。代码里还开了 WAL 模式,进一步提升了并发性能。
数据类型匹配 温度、湿度是简单浮点数,时间戳用 TEXT 存 ISO 格式,足够用。

为什么不选其他数据库

数据库 为什么不选
MySQL / MariaDB / PostgreSQL 太重了。需要安装数据库服务、配置用户权限、维护进程,对树莓派和单传感器场景来说是"杀鸡用牛刀"。
InfluxDB / TimescaleDB 时序数据库确实专业,但适合海量、高并发的时序数据。单个 DHT22 几分钟一条数据,InfluxDB 的优势完全发挥不出来,还要额外安装和维护服务。
Redis Redis 是内存数据库,主打缓存和高速读写,数据持久化能力不如 SQLite,也不适合长期历史数据存储和复杂查询。
CSV / JSON 文件 简单是简单,但没有索引、聚合查询麻烦,数据量大了之后查询很慢,也不利于扩展。
TinyDB 纯 Python 的轻量 NoSQL,也不错,但 SQLite 更成熟、标准、通用,以后迁移到其他语言或工具也更方便。

什么情况下会换别的

  • 数据量很大(比如每秒几十条)、需要复杂分析 → 考虑 InfluxDB / PostgreSQL + TimescaleDB
  • 多台树莓派/传感器要汇总到服务器 → 考虑 PostgreSQL / MySQL / 云端时序数据库
  • 只需要临时缓存最新数据 → Redis
  • 只是临时存几行日志 → CSV/JSON 也行

但现在这种单个树莓派 + 单个 DHT22 + 本地存储 + 简单查询统计的场景,SQLite 就是最合适的,后期扩展到大量传感器时再部署其他数据库。✨

二、实现步骤

(一)SQLite 本地存储模块

DHT22 传感器数据 --- SQLite 本地存储模块,文件名称dht22_db.py。

功能说明:

  1. 使用 SQLite 存储 DHT22 温湿度传感器数据
  2. 每条记录包含数据产生时的精确时间戳(年月日时分秒)
  3. 提供初始化建表、写入、批量查询、统计等基础功能
  4. 轻量级,仅依赖 Python 标准库(无需额外安装)

使用方式:

from dht22_db import Dht22Database

初始化(自动创建表)

db = Dht22Database("/path/to/sensor_data.db")

写入一条记录(时间戳由调用方传入)

db.insert("2026-06-14 01:55:00", 25.8, 67.9)

查询最近 10 条

rows = db.query_recent(10)

统计总数

count = db.count_all()

依赖:

Python 标准库(sqlite3, datetime)

python 复制代码
#!/usr/bin/env python3
import sqlite3
import os
from datetime import datetime

# ==================== SQL 语句 ====================

# 建表语句
CREATE_TABLE_SQL = """
CREATE TABLE IF NOT EXISTS dht22 (
    id          INTEGER PRIMARY KEY AUTOINCREMENT,
    temperature REAL    NOT NULL,
    humidity    REAL    NOT NULL,
    recorded_at TEXT    NOT NULL
)
"""

# 创建索引(按时间查询更快)
CREATE_INDEX_SQL = """
CREATE INDEX IF NOT EXISTS idx_dht22_recorded_at
    ON dht22 (recorded_at DESC)
"""

# 写入语句
INSERT_SQL = """
INSERT INTO dht22 (temperature, humidity, recorded_at)
    VALUES (?, ?, ?)
"""

# 查询最近 N 条
QUERY_RECENT_SQL = """
SELECT id, temperature, humidity, recorded_at
    FROM dht22
    ORDER BY recorded_at DESC
    LIMIT ?
"""

# 按时间段查询
QUERY_RANGE_SQL = """
SELECT id, temperature, humidity, recorded_at
    FROM dht22
    WHERE recorded_at >= ? AND recorded_at <= ?
    ORDER BY recorded_at ASC
"""

# 统计总数
COUNT_ALL_SQL = """
SELECT COUNT(*) FROM dht22
"""

# 统计时间段内数量
COUNT_RANGE_SQL = """
SELECT COUNT(*) FROM dht22
    WHERE recorded_at >= ? AND recorded_at <= ?
"""

# 统计时间段内平均温度/湿度
AVG_RANGE_SQL = """
SELECT AVG(temperature), AVG(humidity)
    FROM dht22
    WHERE recorded_at >= ? AND recorded_at <= ?
"""

# 获取最早和最晚的记录时间
TIME_RANGE_SQL = """
SELECT MIN(recorded_at), MAX(recorded_at) FROM dht22
"""


# ==================== 数据库操作类 ====================

class Dht22Database:
    """
    DHT22 温湿度数据 SQLite 存储

    所有数据以 TEXT 类型存储 ISO 格式时间戳(YYYY-MM-DD HH:MM:SS),
    精确到秒,便于查询和排序。

    用法:
        >>> db = Dht22Database("sensor_data.db")
        >>> db.insert("2026-06-14 01:55:00", 25.8, 67.9)
        >>> db.insert("2026-06-14 02:05:00", 25.6, 68.2)
        >>> rows = db.query_recent(5)
        >>> print(rows[0]["temperature"])
        25.6
    """

    def __init__(self, db_path: str = None):
        """
        初始化数据库连接

        参数:
            db_path: 数据库文件路径。若为 None,默认保存在脚本所在目录
                     若路径不存在,自动创建包含目录和 .db 文件
        """
        if db_path is None:
            script_dir = os.path.dirname(os.path.abspath(__file__))
            db_path = os.path.join(script_dir, "sensor_data.db")

        self.db_path = db_path

        # 自动创建目录(如果路径包含子目录)
        db_dir = os.path.dirname(db_path)
        if db_dir and not os.path.exists(db_dir):
            os.makedirs(db_dir, exist_ok=True)

        # 初始化建表 - 每次调用_init_tables()
        self._init_tables()

    def _get_connection(self) -> sqlite3.Connection:
        """
        获取数据库连接(每次调用创建新连接,确保线程安全)

        返回:
            sqlite3.Connection 对象

        说明:
            SQLite 连接不是线程安全的。每次操作创建新连接,
            避免多线程环境下出现问题。连接用完后由调用方关闭。
        """
        conn = sqlite3.connect(self.db_path)
        # 启用行工厂,返回字典格式结果
        conn.row_factory = sqlite3.Row
        # 启用 WAL 模式,提高并发写入性能
        conn.execute("PRAGMA journal_mode=WAL")
        return conn

    def _init_tables(self):
        """
        仅在表不存在时创建(避免重复执行)

        说明:
            查询 sqlite_master 判断 dht22 表是否已存在,
            只在首次运行或数据库文件丢失时才创建。
        """
        conn = self._get_connection()
        try:
            # 检查 dht22 表是否已存在
            cursor = conn.execute(
                "SELECT name FROM sqlite_master WHERE type='table' AND name='dht22'"
            )
            if cursor.fetchone() is None:
                # 表不存在,创建
                conn.execute(CREATE_TABLE_SQL)
                conn.execute(CREATE_INDEX_SQL)
                conn.commit()
                print(f"✅ [数据库] 首次初始化 {self.db_path}")
            else:
                print(f"✅ [数据库] 已有数据 {self.db_path}")
        finally:
            conn.close()

    def insert(self, recorded_at: str, temperature: float, humidity: float):
        """
        写入一条传感器数据

        参数:
            recorded_at:  数据产生时间,格式 "YYYY-MM-DD HH:MM:SS"
            temperature:  温度值(℃)
            humidity:     湿度值(%RH)

        示例:
            >>> db.insert("2026-06-14 01:55:00", 25.83, 67.92)

        说明:
            recorded_at 由调用方传入,建议使用数据采集时刻的时间,
            而不是数据库写入时间。这样即使上报有延迟,时间戳也准确。
        """
        conn = self._get_connection()
        try:
            conn.execute(INSERT_SQL, (temperature, humidity, recorded_at))
            conn.commit()
        finally:
            conn.close()

    def insert_now(self, temperature: float, humidity: float) -> str:
        """
        写入一条传感器数据(自动使用当前时间)

        参数:
            temperature:  温度值(℃)
            humidity:     湿度值(%RH)

        返回:
            str: 写入的时间戳字符串 "YYYY-MM-DD HH:MM:SS"

        示例:
            >>> ts = db.insert_now(25.8, 67.9)
            >>> print(ts)
            2026-06-14 01:55:00
        """
        now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self.insert(now, temperature, humidity)
        return now

    def query_recent(self, limit: int = 10) -> list:
        """
        查询最近 N 条记录(按时间倒序)

        参数:
            limit: 返回条数,默认 10 条

        返回:
            list[dict]: 每条记录包含 id, temperature, humidity, recorded_at
                        [{'id': 1, 'temperature': 25.8, 'humidity': 67.9,
                          'recorded_at': '2026-06-14 01:55:00'}, ...]

        示例:
            >>> db.query_recent(5)
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(QUERY_RECENT_SQL, (limit,))
            rows = cursor.fetchall()
            return [dict(row) for row in rows]
        finally:
            conn.close()

    def query_range(self, start: str, end: str) -> list:
        """
        查询指定时间范围内的记录(按时间正序)

        参数:
            start: 起始时间 "YYYY-MM-DD HH:MM:SS"
            end:   结束时间 "YYYY-MM-DD HH:MM:SS"

        返回:
            list[dict]: 每条记录包含 id, temperature, humidity, recorded_at

        示例:
            >>> db.query_range("2026-06-14 00:00:00", "2026-06-14 23:59:59")
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(QUERY_RANGE_SQL, (start, end))
            rows = cursor.fetchall()
            return [dict(row) for row in rows]
        finally:
            conn.close()

    def count_all(self) -> int:
        """
        统计总记录数

        返回:
            int: 数据库中总记录条数

        示例:
            >>> total = db.count_all()
            142
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(COUNT_ALL_SQL)
            return cursor.fetchone()[0]
        finally:
            conn.close()

    def count_range(self, start: str, end: str) -> int:
        """
        统计指定时间范围内的记录数

        参数:
            start: 起始时间 "YYYY-MM-DD HH:MM:SS"
            end:   结束时间 "YYYY-MM-DD HH:MM:SS"

        返回:
            int: 该时间段内的记录条数

        示例:
            >>> db.count_range("2026-06-14 00:00:00", "2026-06-14 23:59:59")
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(COUNT_RANGE_SQL, (start, end))
            return cursor.fetchone()[0]
        finally:
            conn.close()

    def avg_range(self, start: str, end: str) -> dict:
        """
        统计指定时间范围内的平均温湿度

        参数:
            start: 起始时间 "YYYY-MM-DD HH:MM:SS"
            end:   结束时间 "YYYY-MM-DD HH:MM:SS"

        返回:
            dict: {"avg_temperature": 25.7, "avg_humidity": 68.1}

        示例:
            >>> avg = db.avg_range("2026-06-14 00:00:00", "2026-06-14 23:59:59")
            {'avg_temperature': 25.72, 'avg_humidity': 68.05}
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(AVG_RANGE_SQL, (start, end))
            row = cursor.fetchone()
            return {
                "avg_temperature": round(row[0], 2) if row[0] else None,
                "avg_humidity":    round(row[1], 2) if row[1] else None
            }
        finally:
            conn.close()

    def get_time_range(self) -> dict:
        """
        查询数据库中最早和最晚的记录时间

        返回:
            dict: {"earliest": "2026-06-14 01:55:00", "latest": "2026-06-14 12:00:00"}
                  空表时返回 None

        示例:
            >>> db.get_time_range()
        """
        conn = self._get_connection()
        try:
            cursor = conn.execute(TIME_RANGE_SQL)
            row = cursor.fetchone()
            if row[0] is None:
                return None
            return {"earliest": row[0], "latest": row[1]}
        finally:
            conn.close()

    def print_stats(self):
        """
        打印数据库统计信息(用于调试和查看)

        示例:
            >>> db.print_stats()

            ════════════════════════════════════
             DHT22 数据库统计
            ════════════════════════════════════
             数据库: /path/to/sensor_data.db
             总记录数: 142 条
            ════════════════════════════════════
        """
        total = self.count_all()
        time_range = self.get_time_range()

        print(f"\n{'='*50}")
        print(f" DHT22 数据库统计")
        print(f"{'='*50}")
        print(f"  数据库: {self.db_path}")
        print(f"  总记录数: {total} 条")
        if time_range:
            print(f"  最早记录: {time_range['earliest']}")
            print(f"  最新记录: {time_range['latest']}")
        print(f"{'='*50}\n")


# ==================== 简单测试 ====================

if __name__ == "__main__":
    import tempfile

    # 使用临时数据库测试
    tmp_path = os.path.join(tempfile.gettempdir(), "dht22_test.db")

    # 如果测试文件已存在,先删除
    if os.path.exists(tmp_path):
        os.remove(tmp_path)

    db = Dht22Database(tmp_path)

    # 写入测试数据
    print("写入测试数据...")
    db.insert("2026-06-14 00:00:00", 25.0, 60.0)
    db.insert("2026-06-14 00:10:00", 25.5, 61.0)
    db.insert("2026-06-14 00:20:00", 26.0, 62.0)
    db.insert("2026-06-14 00:30:00", 26.5, 63.0)
    db.insert_now(25.8, 67.9)

    # 查询测试
    print("\n最近 3 条记录:")
    for row in db.query_recent(3):
        print(f"  {row['recorded_at']} | {row['temperature']}℃ | {row['humidity']}%")

    print(f"\n总记录数: {db.count_all()} 条")

    print(f"\n平均温湿度 (0点~1点):")
    avg = db.avg_range("2026-06-14 00:00:00", "2026-06-14 01:00:00")
    print(f"  平均温度: {avg['avg_temperature']}℃, 平均湿度: {avg['avg_humidity']}%")

    # 打印统计
    db.print_stats()

    # 清理测试文件
    os.remove(tmp_path)
    print("✅ 测试通过,已清理临时文件") 
(二)修改原数据上报模块

腾讯物联网平台 --- DHT22 温湿度传感器 真实数据上报脚本,文件名称iot_dht22_sender.py,导入本地数据库模块

功能说明:

  1. 通过 GPIO 针脚读取 DHT22(AM2302)传感器真实的温度 & 湿度数据
  2. 复用 iot_mqtt.py 模块,连接腾讯云物联网平台并上报数据
  3. 与 iot_sender.py 共用同一份 config.json 配置文件
  4. 使用 adafruit-circuitpython-dht 库(内核级脉冲计数,精度高)
  5. 自带容错:单次读取失败自动重试,连续失败后跳过本次上报
  6. 打印详细的传感器读数日志和上报状态
  7. 本地 SQLite 数据库存储传感器数据(复用 dht22_db.py 模块)
python 复制代码
#!/usr/bin/env python3
# ==================== 导入依赖库 ====================

import time          # 延时、计时
import sys           # 退出程序
import os            # 路径操作

# ---- 导入本项目的 MQTT 公共模块 ----
# iot_mqtt.py 封装了配置读取、MQTT 连接、数据上报等全部逻辑
from iot_mqtt import MqttUploader

# ---- 导入本地数据库模块 ----
# dht22_db.py 封装了 SQLite 存储操作,纯标准库零依赖
from dht22_db import Dht22Database

# ---- 导入 Adafruit DHT 库 ----
# adafruit-circuitpython-dht 使用内核级脉冲计数(pulseio),精度高,稳定可靠
try:
    import board
    import adafruit_dht
    DHT_LIB_AVAILABLE = True
except ImportError:
    DHT_LIB_AVAILABLE = False
    print("⚠️  [警告] adafruit-circuitpython-dht 未安装,将无法读取传感器数据")
    print("   安装方法: pip install adafruit-circuitpython-dht")


# ==================== DHT22 传感器读取类 ====================

class DHT22Reader:
    """
    DHT22(AM2302)温湿度传感器驱动 --- 使用 Adafruit CircuitPython 库

    底层通过内核级脉冲计数(pulseio)与 DHT22 通信,
    精度远高于纯 Python bit-banging,在树莓派 Linux 上稳定可靠。

    使用示例:
        >>> reader = DHT22Reader(pin=4)          # GPIO4 (BCM 编号)
        >>> data = reader.read()                 # 读取一次
        >>> print(data)  # {"temperature": 25.6, "humidity": 62.3}
    """

    # BCM 编号 → adafruit.board 属性名映射
    BCM_TO_BOARD_PIN = {
        2: board.D2,   3: board.D3,   4: board.D4,
        14: board.D14, 15: board.D15,
        17: board.D17, 18: board.D18, 27: board.D27,
        22: board.D22, 23: board.D23, 24: board.D24,
        10: board.D10, 9: board.D9,   25: board.D25,
        11: board.D11, 8: board.D8,    7: board.D7,
        0: board.D0,   1: board.D1,    5: board.D5,
        6: board.D6,   12: board.D12,  13: board.D13,
        19: board.D19, 16: board.D16,  26: board.D26,
        20: board.D20, 21: board.D21,
    }

    # 读取配置
    MAX_RETRIES = 5             # 单次读取失败时的最大重试次数
    RETRY_DELAY = 2.0           # 两次重试之间的等待时间(秒)

    def __init__(self, pin: int):
        """
        初始化 DHT22 传感器读取器

        参数:
            pin: 数据线连接的 GPIO 引脚编号(BCM 编号)
                 例如:pin=4 表示 GPIO4,对应物理引脚 7
        """
        if not DHT_LIB_AVAILABLE:
            raise RuntimeError(
                "adafruit-circuitpython-dht 不可用,无法初始化 DHT22 传感器"
            )

        self.pin = pin                          # GPIO BCM 引脚编号
        self._last_read = None                  # 缓存最近一次成功读取的数据
        self._consecutive_failures = 0          # 连续失败计数(用于日志)

        # 将 BCM 编号映射为 board 属性
        board_pin = self.BCM_TO_BOARD_PIN.get(pin)
        if board_pin is None:
            raise ValueError(f"不支持的 BCM 引脚 {pin},可选: {sorted(self.BCM_TO_BOARD_PIN.keys())}")

        # 创建 DHT22 传感器实例(use_pulseio=True = 内核级高精度脉冲计数)
        self._sensor = adafruit_dht.DHT22(board_pin, use_pulseio=True)

    def read(self) -> dict:
        """
        从 DHT22 传感器读取一次温湿度数据

        返回:
            dict: 成功时返回 {"temperature": 25.6, "humidity": 62.3}
                  失败时返回 None
        """
        if not DHT_LIB_AVAILABLE:
            return None

        try:
            temperature = self._sensor.temperature
            humidity    = self._sensor.humidity

            # Adafruit 库可能返回 None
            if temperature is None or humidity is None:
                return None

            # 合理性检查:温度 -40~80℃,湿度 0~100%
            if not (-40.0 <= temperature <= 80.0):
                print(f"   ⚠️  [异常] 温度值不合理: {temperature}℃")
                return None
            if not (0.0 <= humidity <= 100.0):
                print(f"   ⚠️  [异常] 湿度值不合理: {humidity}%")
                return None

            result = {
                "temperature": round(temperature, 2),
                "humidity":    round(humidity, 2)
            }

            self._last_read = result
            self._consecutive_failures = 0
            return result

        except RuntimeError as e:
            # Adafruit 库读取失败时抛出 RuntimeError
            # 常见原因:DHT22 未响应、校验失败、数据不完整
            print(f"   ⚠️  [传感器异常] {e}")
            return None
        except Exception as e:
            print(f"   ⚠️  [传感器异常] {e}")
            return None

    def read_with_retry(self) -> dict:
        """
        带重试机制的传感器读取

        返回:
            dict: 成功时返回温湿度数据,全部重试失败返回 None

        说明:
            最多重试 MAX_RETRIES 次(默认 3 次),
            每次失败后等待 RETRY_DELAY 秒(默认 2 秒)再重试。
            所有重试失败后,连续失败计数 +1。
        """
        for attempt in range(1, self.MAX_RETRIES + 1):
            result = self.read()
            if result is not None:
                return result
            if attempt < self.MAX_RETRIES:
                print(f"   🔄 [重试] 第 {attempt} 次失败,{self.RETRY_DELAY} 秒后重试...")
                time.sleep(self.RETRY_DELAY)

        self._consecutive_failures += 1
        print(f"   ❌ [读取失败] 已连续失败 {self._consecutive_failures} 次")
        return None

    def cleanup(self):
        """
        清理传感器资源

        说明:
            程序退出时必须调用此方法,释放底层 pulseio 资源。
            否则下次运行时可能因资源被占用而报错。
            
            use_pulseio=True 时,adafruit 库会启动 libgpiod_pulsein64
            子进程。如果主进程被 SIGKILL 杀掉,子进程会变成孤儿进程
            并继续占用 GPIO,导致下次启动失败。因此 cleanup 需要:
            1. 调用 sensor.exit() 正常退出
            2. 强制杀掉残留的 libgpiod_pulsein 进程
        """
        import subprocess

        # 1. 正常退出传感器
        try:
            self._sensor.exit()
        except Exception:
            pass

        # 2. 清理可能残留的 libgpiod_pulsein 子进程
        #    (防止主进程被 SIGKILL 时子进程变成孤儿)
        try:
            subprocess.run(
                ["pkill", "-f", "libgpiod_pulsein"],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,
                timeout=5
            )
        except Exception:
            pass


# ==================== 主函数 ====================

def main():
    """
    程序主入口

    执行流程:
        1. 初始化 DHT22 传感器读取器
        2. 加载 MQTT 配置(复用 iot_mqtt.py)
        3. 连接腾讯云物联网平台
        4. 主循环:定时读取传感器 → 上报数据
        5. 退出时清理 GPIO 和 MQTT 资源
    """

    # ==================== 1. 初始化 DHT22 传感器 ====================

    # ---- 传感器配置 ----
    # DHT22_PIN: 数据线连接的 GPIO BCM 编号
    #   默认 GPIO4 → 物理引脚 7
    #   如需使用其他引脚,修改此处的数字即可
    #   常用可选引脚:GPIO4(7), GPIO17(11), GPIO27(13), GPIO22(15)
    DHT22_PIN = 4

    print(f"\n{'='*55}")
    print(f" 腾讯物联网平台 --- DHT22 温湿度传感器 真实数据上报")
    print(f"{'='*55}")
    print(f"  GPIO 引脚: BCM {DHT22_PIN} (物理引脚 {_bcm_to_physical(DHT22_PIN)})")
    print(f"  传感器类型: DHT22 / AM2302")
    print(f"  接线方式: VCC→3.3V | DATA→GPIO{DHT22_PIN} | GND→GND")
    print(f"  读取方式: adafruit-circuitpython-dht (内核脉冲计数)")
    print(f"{'='*55}")

    # 创建传感器读取器实例
    try:
        dht22 = DHT22Reader(pin=DHT22_PIN)
        print(f"✅ [传感器] DHT22 读取器初始化成功")
    except RuntimeError as e:
        print(f"❌ [传感器] 初始化失败: {e}")
        print(f"   请确认:")
        print(f"   1. 在树莓派上运行本脚本")
        print(f"   2. DHT22 接线正确")
        print(f"   3. 必要时运行: sudo python iot_dht22_sender.py")
        sys.exit(1)

    # ==================== 1.5 初始化本地数据库 ====================

    # 创建数据库实例(默认存储在脚本同目录 sensor_data.db)
    db = Dht22Database()
    print(f"✅ [数据库] SQLite 初始化成功 ({db.db_path})")

    # ==================== 2. 加载 MQTT 配置 & 连接 ====================

    # 创建 MQTT 上传器(自动读取同目录下的 config.json)
    uploader = MqttUploader()

    # 打印启动信息
    uploader.print_startup_info(
        title="DHT22 温湿度传感器 --- 真实数据上报",
        extra_lines=[
            ("数据来源", "DHT22 传感器 (GPIO{})".format(DHT22_PIN)),
            ("上报模式", "真实传感器读数"),
        ]
    )

    # 连接腾讯云物联网平台
    if not uploader.connect():
        print(f"❌ [错误] 无法连接腾讯云,请检查网络和配置")
        dht22.cleanup()
        sys.exit(1)

    # ==================== 3. 注册信号处理 ====================

    def signal_handler(sig, frame):
        """
        Ctrl+C 信号处理函数

        不直接退出,而是将 running 标志设为 False,
        让主循环自然结束,确保资源清理代码被执行。
        """
        print(f"\n🛑 [退出信号] 检测到 Ctrl+C,正在优雅退出...")
        uploader.set_running(False)

    MqttUploader.register_signal_handler(signal_handler)

    # ==================== 4. 主循环 ====================

    print(f"\n📡 [运行中] 开始定时读取 DHT22 并上报数据")
    print(f"   上报间隔: {uploader.report_interval} 秒")
    print(f"   按 Ctrl+C 退出\n")

    report_count     = 0    # 成功上报次数
    skip_count       = 0    # 因传感器读取失败而跳过的次数
    last_successful_read = None  # 最近一次成功读取的数据

    while uploader.is_running():

        # ---- 读取 DHT22 传感器数据 ----
        print(f"--- 第 {report_count + 1} 次读取 ---")
        sensor_data = dht22.read_with_retry()

        if sensor_data is None:
            # 传感器读取失败:跳过本次上报,等待下一轮
            skip_count += 1
            print(f"   ⏭️  [跳过] 本次读数失败,跳过上报(累计跳过 {skip_count} 次)")
        else:
            # 读取成功:保存数据,上报到腾讯云
            last_successful_read = sensor_data
            temp = sensor_data["temperature"]
            humi = sensor_data["humidity"]

            print(f"   🌡️  [传感器读数] 温度: {temp}℃ | 湿度: {humi}%")

            # 写入本地数据库(记录传感器采集时刻的时间戳)
            timestamp = db.insert_now(temp, humi)
            print(f"   💾 [本地数据库] 已写入 {timestamp}")

            # 上报到腾讯云物联网平台
            report_count += 1
            uploader.report(sensor_data)

        # ---- 等待下一次上报 ----
        # 使用小步 sleep(每次 0.5 秒),及时响应 Ctrl+C
        waited = 0
        while uploader.is_running() and waited < uploader.report_interval:
            time.sleep(0.5)
            waited += 0.5

    # ==================== 5. 清理退出 ====================

    print(f"\n{'='*55}")
    print(f" 📊 运行统计")
    print(f"{'='*55}")
    print(f"  成功上报: {report_count} 次")
    print(f"  读取失败跳过: {skip_count} 次")
    if last_successful_read:
        print(f"  最后读数: {last_successful_read['temperature']}℃ / "
              f"{last_successful_read['humidity']}%")
    print(f"{'='*55}")

    # 打印数据库统计
    db.print_stats()

    # 清理传感器
    dht22.cleanup()
    print(f"🧹 [传感器] 已释放 DHT22 资源 (BCM {DHT22_PIN})")

    # 断开 MQTT
    uploader.disconnect()

    print(f"✨ [完成] 程序已安全退出,再见!\n")


# ==================== 辅助函数 ====================

def _bcm_to_physical(bcm_pin: int) -> str:
    """
    将 BCM GPIO 编号转换为物理引脚编号(用于显示,方便接线)

    参数:
        bcm_pin: GPIO BCM 编号(如 4、17、27)

    返回:
        str: 物理引脚编号字符串,如 "7";查不到则返回 "?"+BCM编号

    说明:
        树莓派 40 针 GPIO 引脚映射表。
        只包含常用的 GPIO 引脚,不包含电源和 GND。
    """
    # 树莓派 4B 40 针 GPIO 映射表:{BCM编号: 物理引脚编号}
    BCM_TO_PHYSICAL = {
        2: 3,   3: 5,   4: 7,   14: 8,  15: 10,
        17: 11, 18: 12, 27: 13, 22: 15, 23: 16,
        24: 18, 10: 19, 9: 21,  25: 22, 11: 23,
        8: 24,  7: 26,  0: 27,  1: 28,  5: 29,
        6: 31,  12: 32, 13: 33, 19: 35, 16: 36,
        26: 37, 20: 38, 21: 40,
    }
    phys = BCM_TO_PHYSICAL.get(bcm_pin)
    if phys is not None:
        return str(phys)
    return f"?{bcm_pin}"


# ==================== 程序入口 ====================

if __name__ == "__main__":
    main()
(三)数据库DHT22 传感器数据查询脚本

文件名称为query_sensor_data.py

功能说明:

  1. 读取 sensor_data.db 中的传感器数据并展示
  2. 支持:最近N条、时间段查询、平均统计、导出CSV
  3. 复用 dht22_db.py 模块,不重复数据库逻辑
python 复制代码
运行方式:
   cd /home/zgp/.openclaw/workspace/tencent_iot
   source venv/bin/activate

   # 默认显示最近 10 条
   python query_sensor_data.py

   # 显示最近 20 条
   python query_sensor_data.py -n 20

   # 查询指定时间段
   python query_sensor_data.py -s "2026-06-14 00:00:00" -e "2026-06-14 23:59:59"

   # 显示今日统计
   python query_sensor_data.py --today

   # 导出为 CSV 文件
   python query_sensor_data.py --csv

   # 导出指定时间段为 CSV
   python query_sensor_data.py -s "2026-06-14 00:00:00" -e "2026-06-14 23:59:59" --csv
python 复制代码
import argparse
import sys
import os
from datetime import datetime

# ---- 复用数据库模块 ----
from dht22_db import Dht22Database


def print_rows(rows):
    """格式化打印数据行"""
    if not rows:
        print("  (无数据)")
        return

    # 表头
    print(f"  {'时间':20s}  {'温度':>8s}  {'湿度':>8s}")
    print(f"  {'-'*20}  {'-'*8}  {'-'*8}")

    for r in rows:
        print(f"  {r['recorded_at']}  {r['temperature']:>7.2f}℃  {r['humidity']:>7.1f}%")


def print_summary(db, start=None, end=None, label=""):
    """打印统计摘要"""
    if start and end:
        total = db.count_range(start, end)
        avg = db.avg_range(start, end)
    else:
        total = db.count_all()
        avg = None

    if label:
        print(f"\n{'='*50}")
        print(f" {label}")
        print(f"{'='*50}")

    print(f"  总记录数: {total} 条")

    if total == 0:
        print(f"  (暂无数据)")
        return

    # 时间范围
    tr = db.get_time_range()
    if tr:
        print(f"  最早记录: {tr['earliest']}")
        print(f"  最新记录: {tr['latest']}")

    # 平均温湿度
    if avg and avg['avg_temperature'] is not None:
        print(f"  平均温度: {avg['avg_temperature']}℃")
        print(f"  平均湿度: {avg['avg_humidity']}%")


def export_csv(rows, csv_path):
    """导出数据为 CSV 文件"""
    if not rows:
        print("⚠️  没有数据可导出")
        return

    with open(csv_path, "w", encoding="utf-8") as f:
        f.write("时间,温度(℃),湿度(%RH)\n")
        for r in rows:
            f.write(f"{r['recorded_at']},{r['temperature']},{r['humidity']}\n")

    print(f"✅ 已导出 {len(rows)} 条记录到 {csv_path}")


def get_today_range():
    """获取今天的时间范围"""
    today = datetime.now().strftime("%Y-%m-%d")
    return f"{today} 00:00:00", f"{today} 23:59:59"


def main():
    parser = argparse.ArgumentParser(
        description="DHT22 传感器数据查询工具",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
使用示例:
  python query_sensor_data.py                           # 默认最近 10 条
  python query_sensor_data.py -n 50                     # 最近 50 条
  python query_sensor_data.py --today                   # 今日数据
  python query_sensor_data.py -s "2026-06-14 08:00:00"  # 指定起始时间
  python query_sensor_data.py --csv                     # 导出全部数据为 CSV
        """
    )

    parser.add_argument("-n", "--num", type=int, default=10,
                        help="显示最近 N 条记录(默认 10)")
    parser.add_argument("-s", "--start", type=str, default=None,
                        help='起始时间,格式 "YYYY-MM-DD HH:MM:SS"')
    parser.add_argument("-e", "--end", type=str, default=None,
                        help='结束时间,格式 "YYYY-MM-DD HH:MM:SS"')
    parser.add_argument("--today", action="store_true",
                        help="查询今日数据")
    parser.add_argument("--csv", action="store_true",
                        help="导出为 CSV 文件")
    parser.add_argument("-o", "--output", type=str, default=None,
                        help="CSV 输出路径(默认同目录 sensor_data.csv)")

    args = parser.parse_args()

    # 初始化数据库(自动定位到脚本同目录)
    db = Dht22Database()

    # ---- 确定查询范围 ----
    if args.today:
        start, end = get_today_range()
    elif args.start or args.end:
        start = args.start or "2000-01-01 00:00:00"
        end = args.end or "2099-12-31 23:59:59"
    else:
        start = None
        end = None

    # ---- 查询数据 ----
    if start and end:
        rows = db.query_range(start, end)
        label = f"查询结果 ({start} ~ {end})"
    else:
        rows = db.query_recent(args.num)
        label = f"最近 {args.num} 条记录"

    # ---- CSV 导出 ----
    if args.csv:
        csv_path = args.output or os.path.join(
            os.path.dirname(os.path.abspath(__file__)), "sensor_data.csv"
        )
        # CSV 导出时取全部数据(如果没指定时间范围)
        if not start and not end:
            rows = db.query_recent(999999)  # 取全部
        export_csv(rows, csv_path)
        return

    # ---- 打印结果 ----
    print(f"\n{'='*50}")
    print(f" DHT22 传感器数据查询")
    print(f"{'='*50}")
    print(f"  数据库: {db.db_path}")

    # 打印统计
    print_summary(db, start, end)

    # 打印数据
    print(f"\n{'─'*50}")
    print(f" {label}")
    print(f"{'─'*50}")
    print_rows(rows)

    print(f"{'='*50}\n")


if __name__ == "__main__":
    main()