实时温湿度监测系统:Micropython编码ESP32与DHT22模块的无线数据传输与PC端接收项目

实时温湿度监测系统

前言

人生苦短,我用Python。

由于我在日常工作中经常使用Python,因此在进行该项目时,我首先考虑使用Python进行实现。在搜索电路板编程相关内容时,我发现Micropython是一个非常好的选择,因为它使用简单的语法能够帮助新手快速掌握。因此,我决定使用Micropython来实现该项目。

项目目的

实时监控房间温度,可以将其用作实时温湿度查看的桌面插件,也可以将其用作温湿度监控装置。

要求ESP32所处房间需要有可连接的wifi。

项目材料

  1. ESP32 wifi 模块
  2. HDT22 温湿度传感器
  3. 母对母接头(买HDT22会送)

项目步骤

模拟ESP32接线连接测试

可使用我进行模拟的网站进行学习,点击boot.py再点击播放键即可运行:"Wokwi测试项目"

这个测试网站可以使用"Wokwi-GUEST"开放式wifi进行测试,实际使用中将wifi改为房间中的wifi和密码即可。

并且该项目的两个py文件就是我本地拷录并且运行的代码,代码可以实现持续连接wifi和MQTT的功能,并且有呼吸灯和指示灯(这部分实际连接的时候可以注意到),还有一些数据传输的部分修饰。

能够看到当前的结果就是代码可以正常实现将温湿度以及时间数据传输到MQTT公共服务端:MQTT开放端口

动手实践时可以按照模拟的方式进行实际连接:

搭建PC端ESP32拷录环境

安装tonny并且快速入门可看这个前几集和课件。

【Python+ESP32 快速上手(持续更新中)【 通俗易懂 】】 https://www.bilibili.com/video/BV1G34y1E7tE/?share_source=copy_web\&vd_source=0d6fb1bf666097a8d32dc1f77cf20826

注意事项:

  1. 安装驱动之后连接ESP32到电脑可能不显示端口COM,可能是使用的数据线类型过旧,尽量更换数据线进行使用;
  2. Tonny运行的时候可能出现未连接情况,只需要点击重启后端,或者拔出等几秒重新插入即可。

对ESP32进行拷录

  1. 将模拟网站上的两个代码拷贝下来,修改TOPIC(尽量是唯一的,因为是公共端口,同时记得修改本地接收代码里面的信息)以及wifi部分,上传至ESP32中;
  2. 正确连接HDT22和ESP32;
  3. 给ESP32进行供电,当连接之后蓝灯闪烁就是在上传实时温湿度,蓝灯常亮就是MQTT端口暂时端口,蓝灯不亮就是wifi也没连上;

PC端搭建桌面组件

这部分是主要使用MQTTpython包进行本地数据接收以及tkinter创建桌面组件实现实时展示并且可以绘制折线图。

本地数据接收

MQTT本地包进行实时数据接收,保存到当前目录下的data.txt,可以自行修改,同时记得修改桌面组件读取路径。

python 复制代码
import paho.mqtt.client as mqtt
import json

# 当收到连接时的回调函数
def on_connect(client, userdata, flags, rc):
    print("Connected with result code " + str(rc))
    # 订阅主题
    client.subscribe(topic)

# 当接收到消息时的回调函数
def on_message(client, userdata, msg):
    print("Received message: " + msg.payload.decode())
    dict = json.loads(msg.payload.decode())
    # 将消息保存到文件、数据库等
    with open("data.txt", "a") as file:
        file.write('\t'.join([dict["time"].replace("_"," "),str(dict["temp"]),str(dict["humidity"])])+"\n")

# MQTT Broker的连接参数
broker = "broker.hivemq.com"
port = 1883  # 端口号
topic = "wokwi-weather"  # 订阅的主题,记得修改这里
# 创建一个MQTT客户端
client = mqtt.Client()

# 设置回调函数
client.on_connect = on_connect
client.on_message = on_message

# 连接到MQTT Broker
client.connect(broker, port, 60)

# 开始循环,处理网络流量和调用回调函数
client.loop_forever()

桌面小组件部分

还在不断完善,因为也是刚学tkinter几天没有太掌握。

暂时可以实现实时读取data数据最后并读取全部数据绘制折线图。

python 复制代码
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import pandas as pd

def line_plot():
    # Read the data from the file
    data = pd.read_csv('data.txt', sep='\t', header=None, names=['Timestamp', 'Temperature', 'Humidity'])
    print("Data loaded for plotting.")

    # Create the figure with a single subplot
    fig, ax = plt.subplots(figsize=(12, 6))

    # Plot the temperature
    temperature_line, = ax.plot(data['Timestamp'], data['Temperature'], color='blue', label='Temperature')
    ax.set_xlabel('Timestamp')
    ax.set_ylim(20, 40)  # Set the y-axis limits for temperature to 20-40
    ax.set_ylabel('Temperature (°C)', color='blue')
    ax.tick_params('y', colors='blue')

    
    # Create a twin y-axis for the humidity
    ax2 = ax.twinx()
    humidity_line, = ax2.plot(data['Timestamp'], data['Humidity'], color='green', label='Humidity')
    ax2.set_ylabel('Humidity (%)', color='green')
    ax2.set_ylim(20, 80)  # Set the y-axis limits for humidity to 20-80
    ax2.tick_params('y', colors='green')

    # Set the title and grid
    ax.set_title('Temperature and Humidity over Time')
    ax.grid()

    # Add a legend
    lines = [temperature_line, humidity_line]
    labels = [l.get_label() for l in lines]
    ax.legend(lines, labels, loc='upper left')

    # Display 20 evenly spaced x-axis labels
    num_ticks = 20
    start = 0
    end = len(data['Timestamp'])
    tick_locations = [start + i * (end - start) / (num_ticks - 1) for i in range(num_ticks)]
    # def split_timestamp(ts):
    #     return "-".join(":".join(ts.split(":")[:-1]).split("-")[:])

    # tick_locations = tick_locations.apply(split_timestamp)
    tick_locations = [int(loc) for loc in tick_locations]
    ax.set_xticks(tick_locations)
    plt.setp(ax.get_xticklabels(), rotation=30)
    plt.tight_layout()
    # Display the plot
    return fig

class AutoHideWindow:
    def __init__(self, root):
        self.root = root
        self.root.geometry("320x130-100+100")
        self.root.overrideredirect(True)
        self.root.wm_attributes("-topmost", True)
        self.root.wm_attributes("-alpha", 0.9)
        self.is_hidden = False

        self.screen_width = self.root.winfo_screenwidth()
        self.screen_height = self.root.winfo_screenheight()

        self.hidden_window = None
        self.line_chart_window = None
        self.line_chart_open = False  # Track if the line chart window is open

        self.create_main_interface()
        self.create_line_chart_window()

        self.root.bind("<Configure>", self.check_position)
        self.root.bind("<Enter>", self.show_full_window)
        self.root.bind("<Escape>", self.hide_window)
        self.root.bind("<Return>", self.show_full_window)
        self.root.bind("<ButtonPress-1>", self.start_move)
        self.root.bind("<B1-Motion>", self.on_move)

        self.x_offset = 0
        self.y_offset = 0

        self.update_data()

    def create_main_interface(self):
        self.main_frame = ttk.Frame(self.root)
        self.main_frame.pack(fill=tk.BOTH, expand=True)

        self.gif_label = tk.Label(self.main_frame)
        self.gif_label.grid(row=0, column=1, rowspan=4, padx=5, pady=5, sticky=tk.W)
        self.load_gif("功德加一+(1).gif")

        self.numbers_label = ttk.Frame(self.main_frame)
        self.numbers_label.grid(row=0, column=0, rowspan=3, padx=10, pady=10)

        self.number0_label = tk.Label(self.numbers_label, width=20, height=1, bg='green', fg='white', font="Arial 10 bold", text=" ", relief=tk.FLAT, anchor=tk.W)
        self.number0_label.grid(column=0, row=0, sticky=tk.E)

        self.number1_label = tk.Label(self.numbers_label, width=20, height=1, bg='white', fg='black', font="Arial 10", text="温度:", relief=tk.FLAT, anchor=tk.W)
        self.number1_label.grid(column=0, row=1, sticky=tk.E, ipady=3)

        self.number2_label = tk.Label(self.numbers_label, width=20, height=1, bg='white', fg='black', font="Arial 10", text="湿度:", relief=tk.FLAT, anchor=tk.W)
        self.number2_label.grid(column=0, row=2, sticky=tk.E, ipady=3)

        self.button = ttk.Button(self.main_frame, text="温湿度折线图", command=self.show_line_chart_window)
        self.button.grid(column=0, row=3, sticky=tk.E)

    def load_gif(self, path):
        self.gif = Image.open(path)
        self.gif_frames = []
        try:
            while True:
                self.gif_frames.append(ImageTk.PhotoImage(self.gif.copy()))
                self.gif.seek(len(self.gif_frames))
        except EOFError:
            pass

        self.current_frame = 0
        self.update_gif()

    def update_gif(self):
        self.gif_label.configure(image=self.gif_frames[self.current_frame])
        self.current_frame = (self.current_frame + 1) % len(self.gif_frames)
        self.root.after(100, self.update_gif)

    def create_line_chart_window(self):
        x, y = self.root.winfo_x(), self.root.winfo_y()
        width, height = 10, self.root.winfo_height()

        self.line_chart_window = tk.Toplevel(self.root)
        self.line_chart_window.geometry(f"320x500+{x}+{y}")
        self.line_chart_window.withdraw()

        # Bind the close event of the window to a method that resets the open status
        self.line_chart_window.protocol("WM_DELETE_WINDOW", self.close_line_chart_window)

    def check_position(self, event=None):
        if self.is_hidden:
            return
        x, y = self.root.winfo_x(), self.root.winfo_y()
        width, height = self.root.winfo_width(), self.root.winfo_height()
        if x <= 0 or x + width >= self.screen_width:
            self.hide_window()

    def hide_window(self, event=None):
        if self.hidden_window or self.is_hidden:
            return

        x, y = self.root.winfo_x(), self.root.winfo_y()
        width, height = 10, self.root.winfo_height()

        self.hidden_window = tk.Toplevel(self.root)
        self.hidden_window.geometry(f"{width}x{height}+{x}+{y}")
        self.hidden_window.overrideredirect(True)
        self.hidden_window.bind("<Enter>", self.show_full_window)

    def show_full_window(self, event=None):
        if self.hidden_window:
            self.hidden_window.destroy()
            self.hidden_window = None
            self.root.deiconify()
            self.is_hidden = False

    def show_line_chart_window(self):
        if self.line_chart_open:
            self.line_chart_window.deiconify()  # Show existing window
            self.create_line_chart(self.line_chart_window)  # Redraw the chart
        else:
            self.create_line_chart(self.line_chart_window)
            self.line_chart_window.deiconify()
            self.line_chart_open = True  # Update the open status

    def close_line_chart_window(self):
        if self.line_chart_open:
            self.line_chart_window.withdraw()  # Hide the window
            self.line_chart_open = False  # Update the open status

    def start_move(self, event):
        self.x_offset = event.x
        self.y_offset = event.y

    def on_move(self, event):
        x = self.root.winfo_pointerx() - self.x_offset
        y = self.root.winfo_pointery() - self.y_offset
        self.root.geometry(f"+{x}+{y}")

    def update_data(self, file="data.txt"):
        try:
            with open(file, "r") as file:
                lines = file.readlines()
                if lines:
                    last_line = lines[-1]
                    lasttime, temperate0, humi = last_line.split('\t')
                    temperate = temperate0.strip("℃ ")

                    self.number0_label.config(text=f"时间:{' '.join(lasttime.split('_'))}")
                    self.number1_label.config(text=f"温度:{temperate}℃")
                    self.number2_label.config(text=f"湿度:{humi.strip()}%")
        except Exception as e:
            print(f"读取文件出错: {e}")

        self.root.after(10000, self.update_data)

    def create_line_chart(self, window):
        fig = line_plot()

        canvas = FigureCanvasTkAgg(fig, master=window)
        canvas.draw()
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)

if __name__ == "__main__":
    root = tk.Tk()
    app = AutoHideWindow(root)
    root.mainloop()

这两个代码要同时运行就可以实现实时接收数据和实时组件展示,只开第一个就可以实时接收数据。

实验总结

是一次很好的学习电路板模块的小项目,也可作为中学生实践课程项目。

希望大家多多交流讨论啊,本人也是新手,希望有更简单高效的解决方案。

相关推荐
东京老树根32 分钟前
SAP学习笔记 - 开发27 - 前端Fiori开发 Routing and Navigation(路由和导航)
笔记·学习
阿阳微客7 小时前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏
Chef_Chen11 小时前
从0开始学习R语言--Day18--分类变量关联性检验
学习
键盘敲没电12 小时前
【IOS】GCD学习
学习·ios·objective-c·xcode
海的诗篇_12 小时前
前端开发面试题总结-JavaScript篇(一)
开发语言·前端·javascript·学习·面试
AgilityBaby12 小时前
UE5 2D角色PaperZD插件动画状态机学习笔记
笔记·学习·ue5
AgilityBaby12 小时前
UE5 创建2D角色帧动画学习笔记
笔记·学习·ue5
武昌库里写JAVA14 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
一弓虽15 小时前
git 学习
git·学习
Moonnnn.17 小时前
【单片机期末】串行口循环缓冲区发送
笔记·单片机·嵌入式硬件·学习