這個程式是基於 ESP32 的 MicroPython 應用,用於偵測人體紅外線感應器(PIR)的移動,並通過 TM1637 LED 顯示模組顯示計時。
主要功能包括:
移動偵測與計時:
當 PIR 感應器偵測到移動時,開始計時,並在 LED 顯示模組上顯示經過的時間(格式為 MM
)。
如果持續偵測到移動,計時會繼續,直到達到設定的最大時間(例如 30 分鐘)。
當計時達到最大時間時,顯示 “UPUP” 字樣,提示使用者需要進行某種操作。
重置功能:
當顯示 “UPUP” 時,按下連接到 GPIO12 的按鈕可以重置計時,回到等待偵測的狀態。
無移動倒計時:
如果在移動偵測後的持續時間內未再偵測到移動,開始 30 秒的倒計時。
如果在倒計時結束前未偵測到新的移動,計時停止,回到等待狀態。
MQTT 通訊:
程式會通過 MQTT 將狀態和數據上傳到伺服器,包括偵測狀態、計時、IP 地址等。
時間校正:
通過 NTP 進行時間同步,確保裝置的時間正確。
每天固定時間(例如凌晨 4 點)進行一次時間校正。
python
複製程式碼
# 2024/10/06-12:30
import network
import time
import ujson
from machine import Pin, RTC
from umqtt.simple import MQTTClient
import ntptime # 用於 NTP 時間校正
import tm1637 # 引入 tm1637 驅動程式
# Wi-Fi 設定
SSID = "Bili-Net"
PASSWORD = "0932388283"
HOSTNAME = "MotionTimer-01"
mqtt_server = '192.168.0.172'
mqtt_port = 11883 # MQTT 伺服器端口
client_id = HOSTNAME + '_client'
mqtt_topic = 'myTopic'
# 台北時區 (UTC+8)
TIMEZONE_OFFSET = 8 * 60 * 60 # 秒
# 定義 HC-SR501 的輸出腳位、LED 顯示模組的 CLK 和 DIO 腳位,以及重置按鈕
PIR_PIN = Pin(4, Pin.IN) # ESP32 的 D4 腳位對應 GPIO4,用於接收 PIR 感應器訊號
LED_PIN = Pin(2, Pin.OUT) # GPIO2 用來控制 LED 指示燈
RESET_BUTTON = Pin(12, Pin.IN, Pin.PULL_UP) # GPIO12 接重置按鈕,內部上拉
tm = tm1637.TM1637(clk=Pin(16), dio=Pin(17)) # 初始化 TM1637 顯示模組
rtc = RTC() # 初始化實時時鐘
# 設定相關變數
detect_flag = False # 是否偵測到物體的標誌
start_time = 0 # 計時開始時間
timeout = 45 # 無移動時的倒計時,單位:秒
interval = 1 # 主迴圈的延遲時間,單位:秒
no_motion_start_time = None # 記錄無移動開始的時間
max_time = 1800 # 計時最大值為 30 分鐘 (1800 秒)
last_ntp_sync = None # 上次 NTP 同步時間
mqtt_client = None # MQTT 客戶端
mqtt_reconnect_interval = 10 # MQTT 重連間隔時間,單位:秒
# 格式化當前日期和時間為 'YYYY-MM-DD HH:MM:SS'
def format_datetime():
now = time.localtime(time.time() + TIMEZONE_OFFSET) # 調整為台北時區
return "{:04d}-{:02d}-{:02d} {:02d}:{:02d}:{:02d}".format(
now[0], now[1], now[2], now[3], now[4], now[5]
)
# NTP 校正時間
def sync_time():
global last_ntp_sync
try:
ntptime.settime()
last_ntp_sync = time.time() + TIMEZONE_OFFSET # 記錄最後同步時間
print(f"時間校正成功: {format_datetime()}")
except Exception as e:
print(f"時間校正失敗: {e}")
# 檢查是否需要進行 NTP 校正
def check_time_sync():
global last_ntp_sync
now = time.localtime(time.time() + TIMEZONE_OFFSET)
# 每天早上 4:00 校正時間
if now[3] == 4 and (last_ntp_sync is None or time.time() - last_ntp_sync >= 86400):
sync_time()
# 建立 MQTT 連接
def connect_mqtt():
global mqtt_client
try:
mqtt_client = MQTTClient(client_id, mqtt_server, port=mqtt_port)
mqtt_client.connect()
print("已連接到 MQTT 伺服器")
except Exception as e:
mqtt_client = None
print(f"無法連接到 MQTT 伺服器: {e}")
# 連接到 Wi-Fi
def connect_wifi():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
wlan.connect(SSID, PASSWORD)
while not wlan.isconnected():
print("正在連接到 Wi-Fi...")
time.sleep(1)
print("Wi-Fi 連接成功,網路設定:", wlan.ifconfig())
sync_time() # Wi-Fi 連接成功後校正時間
# 上傳數據到 MQTT
def publish_data(status):
global mqtt_client
if mqtt_client:
try:
wlan = network.WLAN(network.STA_IF) # 確保已經連接到 Wi-Fi
ip_address = wlan.ifconfig()[0] # 取得 IP 地址
data = {
"hostname": HOSTNAME,
"status": status,
"timestamp": format_datetime(), # 使用格式化的時間
"ip": ip_address # 新增 IP 地址
}
mqtt_client.publish(mqtt_topic, ujson.dumps(data))
print(f"已上傳數據: {data}")
except Exception as e:
print(f"上傳數據失敗: {e}")
mqtt_client = None # 失敗後重置 mqtt_client 並重新連接
else:
print("MQTT 未連接,嘗試重新連接...")
connect_mqtt()
# 顯示等待狀態(中間的冒號)
def display_waiting():
tm.set_segments([0x00, 0x80, 0x00, 0x00]) # 中間顯示冒號
# 顯示時間格式 MM:SS
def display_time(minutes, seconds):
minute_tens = minutes // 10
minute_ones = minutes % 10
second_tens = seconds // 10
second_ones = seconds % 10
segments = [
tm.encode_digit(minute_tens),
tm.encode_digit(minute_ones) | 0x80, # 中間的冒號
tm.encode_digit(second_tens),
tm.encode_digit(second_ones)
]
tm.set_segments(segments)
# 顯示秒數格式 :SS
def display_seconds(seconds):
second_tens = seconds // 10
second_ones = seconds % 10
segments = [0x00, 0x80, tm.encode_digit(second_tens), tm.encode_digit(second_ones)]
tm.set_segments(segments)
# 顯示 'UPUP' 字樣
def display_upup():
segments = [0x3e, 0x73, 0x3e, 0x73] # 'UPUP' 的字模
tm.set_segments(segments)
# 重置計時器並回到等待狀態
def reset_timer():
global detect_flag, start_time, no_motion_start_time
detect_flag = False
start_time = 0
no_motion_start_time = None
LED_PIN.off() # 熄滅指示燈
display_waiting() # 回到等待模式
print("計時已重置,回到等待模式")
# 主程式
try:
connect_wifi() # 連接 Wi-Fi
connect_mqtt() # 連接 MQTT
print("PIR 模組初始化中...")
time.sleep(2) # 等待 PIR 模組穩定
print("準備偵測中(按 Ctrl+C 結束)")
display_waiting() # 開始時顯示等待狀態
while True:
check_time_sync() # 檢查是否需要校正時間
current_state = PIR_PIN.value() # 讀取 PIR 感應器狀態
# 檢查是否按下重置按鈕(GPIO12 為低時表示按下)
if RESET_BUTTON.value() == 0:
reset_timer()
publish_data("reset")
time.sleep(0.5) # 防止按鈕彈跳
if current_state:
if not detect_flag:
detect_flag = True
start_time = time.time()
no_motion_start_time = None
LED_PIN.on() # 開啟指示燈
print("\n偵測到物體移動! 開始計時...", end="\r")
publish_data("detected")
else:
elapsed_time = time.time() - start_time
if elapsed_time >= max_time:
print("計時達到30分鐘,顯示UPUP...")
while PIR_PIN.value():
# 新增:檢查重置按鈕是否被按下
if RESET_BUTTON.value() == 0:
reset_timer()
publish_data("reset")
time.sleep(0.5) # 防止按鈕彈跳
break # 跳出迴圈,停止顯示UPUP
display_upup()
publish_data("UPUP")
time.sleep(1)
tm.clear() # 清除顯示
time.sleep(1)
reset_timer()
else:
minutes = int(elapsed_time // 60)
seconds = int(elapsed_time % 60)
display_time(minutes, seconds)
publish_data(f"{minutes:02}:{seconds:02}")
print(f"持續偵測到物體移動! 計時... {minutes:02}:{seconds:02}", end="\r")
no_motion_start_time = None
else:
if detect_flag and no_motion_start_time is None:
no_motion_start_time = time.time()
print("\n未偵測到移動,開始計算30秒倒數...", end="\r")
publish_data("no_motion")
elif no_motion_start_time is not None:
no_motion_elapsed_time = time.time() - no_motion_start_time
if no_motion_elapsed_time >= timeout:
elapsed_time = time.time() - start_time
seconds = int(elapsed_time % 60)
display_seconds(seconds)
print(f"\n30秒內未偵測到移動,停止計時... 偵測停止")
reset_timer()
publish_data("stopped")
else:
seconds = int(timeout - no_motion_elapsed_time)
display_seconds(seconds)
publish_data(f"countdown: {seconds} seconds")
print(f"\n30秒倒數中,還剩:{seconds} 秒", end="\r")
# 每隔1秒檢查一次狀態,更新時間顯示
time.sleep(interval)
except KeyboardInterrupt:
print("\n程式結束")
按鈕防抖處理:
問題: 目前的按鈕按下後只使用了簡單的延遲來防止彈跳,可能不夠可靠。
建議: 實現一個軟體防抖機制,例如在檢測到按鈕狀態改變後,連續檢查按鈕狀態數次,確保按鈕確實被按下。
錯誤處理和穩定性:
問題: 在連接 MQTT 和 Wi-Fi 時,缺乏重新嘗試機制。
建議: 實現自動重連功能,當連接失敗時,間隔一段時間後重新嘗試連接,確保裝置的穩定性。
資源管理:
問題: 長時間運行可能導致記憶體洩漏或資源耗盡。
建議: 定期清理未使用的資源,或在需要時進行垃圾回收。
提升可讀性和維護性:
問題: 部分程式碼缺乏適當的結構化。
建議: 將重複的代碼段抽象為函數,並添加更多註解,以提高可讀性。
安全性考量:
問題: Wi-Fi 密碼和 MQTT 伺服器資訊在程式中明文顯示。
建議: 將敏感資訊存儲在配置文件或環境變數中,或在發布前移除敏感資訊。
硬體兼容性:
注意事項: 不同的硬體可能有不同的引腳配置。
建議: 在部署到新硬體時,確認引腳配置是否正確,並根據需要進行調整。
時間同步優化:
問題: 每天只在固定時間同步時間,可能導致時間累積誤差。
建議: 增加同步頻率,例如每隔 12 小時同步一次,或者當偵測到時間差異較大時自動同步。
日誌記錄和監控:
建議: 添加日誌記錄功能,將關鍵事件和錯誤記錄下來,以便於日後的問題排查。
用戶反饋:
建議: 在裝置上添加更多的用戶反饋機制,例如蜂鳴器或 LED 指示燈,以提示當前狀態。
功耗管理:
注意事項: 如果裝置需要長時間運行,功耗可能是個問題。
建議: 考慮在無需運行時進入休眠模式,或優化程式以降低功耗。