Files
Competition-2026/onboardprogram/main.py
T

710 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
# 智能生活主机 main.py(mPython / 掌控板 MicroPython)
# 功能:P0人体感应、P7 RFID门灯、P13/P14两路照明联动、P5/P6风扇、
# SHT20温湿度、UDP心率/摔倒从机、摇晃报警、HTTP API、Wi-Fi失败后启动AP。
# 注意:心率由从机模拟;摇晃报警是演示级“疑似摔倒”判断,不是医学级跌倒检测。
# 电机/灯具必须通过驱动板、MOSFET或继电器供电,不能直接由GPIO带负载。
from mpython import *
from machine import Pin
from bluebit import SHT20, Scan_Rfid
import socket
import time
import json
# ==================== 配置 ====================
WIFI_SSID = "48-502"
WIFI_PASSWORD = "a13356041516"
AP_SSID = "QHZXManagement"
AP_PASSWORD = "QHZX2026"
WIFI_CONNECT_TIMEOUT = 8
PIR_PIN_NO = 0
DOOR_LIGHT_PIN_NO = 7
ROOM_LIGHT_PIN_NOS = (13, 14)
FAN_PIN_A_NO = 5
FAN_PIN_B_NO = 6
RFID_AUTHORIZED_UID = 1239985986
DEFAULT_FAN_TEMP = 28.0
DEFAULT_FAN_HUMIDITY = 75.0
DEFAULT_LIGHT_TIMEOUT = 30
DEFAULT_DOOR_TIMEOUT = 8
MASTER_UDP_PORT = 4210
SLAVE_UDP_PORT = 4211
UDP_STATUS_TIMEOUT_MS = 5000
# 摇晃检测:100ms采样。
# 连续两次明显变化,或一次强烈变化,触发报警。
SHAKE_DELTA_SQ = 0.65
SHAKE_STRONG_DELTA_SQ = 2.0
SHAKE_WINDOW_MS = 900
SHAKE_COOLDOWN_MS = 5000
fan_auto = True
pir_auto = True
fan_temp_threshold = DEFAULT_FAN_TEMP
fan_humidity_threshold = DEFAULT_FAN_HUMIDITY
light_timeout = DEFAULT_LIGHT_TIMEOUT
door_timeout = DEFAULT_DOOR_TIMEOUT
# ==================== 状态变量 ====================
fallen = False
fall_count = 0
fall_time = 0
fall_source = ""
last_accel = None
last_local_sample_ms = 0
last_local_fall_event_ms = None
shake_hit_count = 0
last_shake_hit_ms = None
temperature = None
humidity = None
sensor_ok = False
last_sensor_ms = 0
pir_now = False
last_motion_ms = time.ticks_ms()
door_light_deadline = None
rfid_last = "—"
rfid_authorized = False
rfid_card_present = False
rfid_available = True
heart_rate_bpm = None
heart_rate_simulated = True
slave_fall_alarm = False
slave_fall_count = 0
slave_connected = False
slave_last_rx_ms = None
last_slave_fall_alarm = False
slave_ip = ""
slave_peer_addr = None
slave_udp = None
wifi_mode = "STA"
ip = "0.0.0.0"
pin_warnings = []
# ==================== GPIO初始化 ====================
def add_warning(message):
if message not in pin_warnings:
pin_warnings.append(message)
def make_pin(pin_number, mode, pull=None):
try:
pin = MPythonPin(pin_number, mode, pull)
if mode == PinMode.OUT:
pin.write_digital(0)
return pin
except Exception as e:
message = "P%d不可用: %s" % (pin_number, str(e))
print(message)
add_warning(message)
return None
pir_pin = make_pin(PIR_PIN_NO, PinMode.IN, Pin.PULL_DOWN)
door_light_pin = make_pin(DOOR_LIGHT_PIN_NO, PinMode.OUT)
room_light_pins = [make_pin(n, PinMode.OUT) for n in ROOM_LIGHT_PIN_NOS]
fan_pin_a = make_pin(FAN_PIN_A_NO, PinMode.OUT)
fan_pin_b = make_pin(FAN_PIN_B_NO, PinMode.OUT)
fan_available = fan_pin_a is not None and fan_pin_b is not None
room_light_available = all(pin is not None for pin in room_light_pins)
outputs = {"door_light": False, "room_light": False, "fan": False}
try:
sht20 = SHT20()
except Exception as e:
sht20 = None
add_warning("SHT20初始化失败: %s" % str(e))
# ==================== 输出控制 ====================
def set_door_light(state):
if door_light_pin is None:
outputs["door_light"] = False
return False
try:
door_light_pin.write_digital(1 if state else 0)
outputs["door_light"] = bool(state)
return True
except Exception as e:
add_warning("P7门灯输出失败: %s" % str(e))
outputs["door_light"] = False
return False
def set_room_lights(state):
"""P13/P14作为同一组灯,调用一次就同时开或同时关。"""
results = []
for index, pin in enumerate(room_light_pins):
if pin is None:
results.append(False)
continue
try:
pin.write_digital(1 if state else 0)
results.append(True)
except Exception as e:
add_warning("P%d照明输出失败: %s" % (ROOM_LIGHT_PIN_NOS[index], str(e)))
results.append(False)
outputs["room_light"] = bool(state) and len(results) == 2 and all(results)
if not all(results):
add_warning("照明灯组存在不可用引脚,请检查P13/P14及板卡版本")
return len(results) == 2 and all(results)
def set_fan(state):
global fan_available
if not fan_available:
outputs["fan"] = False
return False
try:
if state:
fan_pin_a.write_digital(1)
fan_pin_b.write_digital(0)
else:
fan_pin_a.write_digital(0)
fan_pin_b.write_digital(0)
outputs["fan"] = bool(state)
return True
except Exception as e:
fan_available = False
outputs["fan"] = False
add_warning("P%d/P%d风扇控制失败: %s" % (FAN_PIN_A_NO, FAN_PIN_B_NO, str(e)))
return False
# ==================== 摔倒/摇晃报警 ====================
def register_fall(source, now=None):
"""锁存一次疑似摔倒报警,直到App调用/api/reset。"""
global fallen, fall_count, fall_time, fall_source
if fallen:
return False
if now is None:
now = time.ticks_ms()
fallen = True
fall_count += 1
fall_time = now // 1000
fall_source = source
print("摔倒报警:", source)
return True
def detect_shake(now):
"""根据加速度相邻采样变化判断摇晃;触发后保持fallen=True。"""
global last_accel, last_local_sample_ms
global last_local_fall_event_ms, shake_hit_count, last_shake_hit_ms
if time.ticks_diff(now, last_local_sample_ms) < 100:
return
last_local_sample_ms = now
try:
accel = (
accelerometer.get_x(),
accelerometer.get_y(),
accelerometer.get_z()
)
if last_accel is not None:
dx = accel[0] - last_accel[0]
dy = accel[1] - last_accel[1]
dz = accel[2] - last_accel[2]
change_sq = dx * dx + dy * dy + dz * dz
if change_sq >= SHAKE_DELTA_SQ:
if (last_shake_hit_ms is None or
time.ticks_diff(now, last_shake_hit_ms) > SHAKE_WINDOW_MS):
shake_hit_count = 1
else:
shake_hit_count += 1
last_shake_hit_ms = now
confirmed = (change_sq >= SHAKE_STRONG_DELTA_SQ or shake_hit_count >= 2)
cooldown_ok = (
last_local_fall_event_ms is None or
time.ticks_diff(now, last_local_fall_event_ms) >= SHAKE_COOLDOWN_MS
)
if confirmed and cooldown_ok and not fallen:
if register_fall("主机摇晃检测(疑似摔倒)", now):
last_local_fall_event_ms = now
shake_hit_count = 0
elif (last_shake_hit_ms is not None and
time.ticks_diff(now, last_shake_hit_ms) > SHAKE_WINDOW_MS):
shake_hit_count = 0
last_accel = accel
except Exception as e:
print("主机加速度计读取异常:", e)
# ==================== UDP主从通信 ====================
def udp_send_slave_command(command):
if slave_udp is None or slave_peer_addr is None:
return False
try:
payload = json.dumps(command).encode("utf-8")
for _ in range(3):
slave_udp.sendto(payload, slave_peer_addr)
time.sleep_ms(30)
return True
except Exception as e:
add_warning("UDP发送从机命令失败: %s" % str(e))
return False
def handle_slave_message(message, addr=None):
global heart_rate_bpm, heart_rate_simulated
global slave_fall_alarm, slave_fall_count, slave_connected
global slave_last_rx_ms, last_slave_fall_alarm
global slave_ip, slave_peer_addr
if not isinstance(message, dict) or message.get("type") != "slave_status":
return
now = time.ticks_ms()
slave_last_rx_ms = now
slave_connected = True
if addr is not None:
slave_ip = addr[0]
slave_peer_addr = (addr[0], SLAVE_UDP_PORT)
bpm = message.get("heart_rate_bpm")
if isinstance(bpm, (int, float)):
heart_rate_bpm = int(bpm)
heart_rate_simulated = bool(message.get("heart_rate_simulated", True))
try:
slave_fall_count = int(message.get("fall_count", slave_fall_count))
except Exception:
pass
new_alarm = bool(message.get("fall_alarm", False))
slave_fall_alarm = new_alarm
if new_alarm and not last_slave_fall_alarm:
register_fall("从机摇晃检测(疑似摔倒)", now)
last_slave_fall_alarm = new_alarm
def poll_slave_udp():
global slave_connected
if slave_udp is None:
slave_connected = False
return
for _ in range(6):
try:
packet, addr = slave_udp.recvfrom(1024)
except OSError:
break
except Exception as e:
add_warning("UDP接收异常: %s" % str(e))
break
try:
message = json.loads(packet.decode("utf-8", "ignore"))
handle_slave_message(message, addr)
except Exception as e:
print("从机UDP数据解析异常:", e)
if slave_last_rx_ms is None:
slave_connected = False
else:
slave_connected = time.ticks_diff(time.ticks_ms(), slave_last_rx_ms) < UDP_STATUS_TIMEOUT_MS
# ==================== HTTP API ====================
def parse_query(query_string):
result = {}
if not query_string:
return result
for item in query_string.split("&"):
if "=" in item:
key, value = item.split("=", 1)
result[key] = value.replace("+", " ")
elif item:
result[item] = ""
return result
def status_data():
return {
"ok": True,
"message": "ok",
"temperature": temperature,
"humidity": humidity,
"sensor_ok": bool(sensor_ok),
"pir": bool(pir_now),
"pir_auto": bool(pir_auto),
"door_light": bool(outputs["door_light"]),
"room_light": bool(outputs["room_light"]),
"fan": bool(outputs["fan"]),
"fan_auto": bool(fan_auto),
"fan_available": bool(fan_available),
"room_light_available": bool(room_light_available),
"fan_temp_threshold": fan_temp_threshold,
"fan_humidity_threshold": fan_humidity_threshold,
"light_timeout": light_timeout,
"door_timeout": door_timeout,
"rfid_last": str(rfid_last),
"rfid_authorized": bool(rfid_authorized),
"rfid_available": bool(rfid_available),
"heart_rate_bpm": heart_rate_bpm,
"heart_rate_simulated": bool(heart_rate_simulated),
"slave_connected": bool(slave_connected),
"slave_transport": "UDP/Wi-Fi",
"slave_ip": slave_ip,
"slave_udp_port": SLAVE_UDP_PORT,
"slave_fall_alarm": bool(slave_fall_alarm),
"slave_fall_count": slave_fall_count,
"fall_source": fall_source,
"fall_alert": bool(fallen),
"fall_status": "摔倒报警" if fallen else "正常",
"shake_detection": True,
"pin_status": ";".join(pin_warnings) if pin_warnings else "所有已配置引脚初始化正常",
"uptime": time.ticks_ms() // 1000,
"fallen": bool(fallen),
"fall_count": fall_count,
"fall_time": fall_time,
"wifi_mode": wifi_mode,
"ip_address": ip,
"ap_ssid": AP_SSID,
"ap_password": AP_PASSWORD if wifi_mode == "AP" else ""
}
def send_json(conn, data, status_code="200 OK"):
body = json.dumps(data).encode("utf-8")
header = (
"HTTP/1.1 %s\r\n" % status_code
+ "Content-Type: application/json; charset=utf-8\r\n"
+ "Access-Control-Allow-Origin: *\r\n"
+ "Access-Control-Allow-Methods: GET, OPTIONS\r\n"
+ "Cache-Control: no-store\r\n"
+ "Connection: close\r\n"
+ "Content-Length: %d\r\n\r\n" % len(body)
)
try:
conn.send(header.encode("utf-8") + body)
except Exception:
pass
def control_device(params):
global fan_auto, pir_auto, door_light_deadline, last_motion_ms
device = params.get("device", "")
state = params.get("state", "0") in ("1", "true", "on")
if device == "fan":
fan_auto = False
if not fan_available:
return False, "风扇驱动不可用,请检查P5/P6与驱动板"
if not set_fan(state):
return False, "风扇控制失败,请检查电机驱动模块"
return True, "风扇状态已更新"
if device == "room_light":
pir_auto = False
if not set_room_lights(state):
return False, "照明灯组未能全部切换,请检查P13/P14"
if state:
last_motion_ms = time.ticks_ms()
return True, "两路照明已同步打开" if state else "两路照明已同步关闭"
if device == "door_light":
door_light_deadline = None
if not set_door_light(state):
return False, "P7门灯输出不可用,请检查接线"
return True, "门上灯状态已更新"
return False, "未知设备名称"
def update_settings(params):
global pir_auto, fan_auto
global fan_temp_threshold, fan_humidity_threshold
global light_timeout, door_timeout
try:
if "pir_auto" in params:
pir_auto = params["pir_auto"] in ("1", "true", "on")
if "fan_auto" in params:
fan_auto = params["fan_auto"] in ("1", "true", "on")
if "fan_temp" in params:
value = float(params["fan_temp"])
if not 15 <= value <= 40:
return False, "温度阈值范围为15到40°C"
fan_temp_threshold = value
if "fan_humidity" in params:
value = float(params["fan_humidity"])
if not 30 <= value <= 95:
return False, "湿度阈值范围为30到95%"
fan_humidity_threshold = value
if "light_timeout" in params:
light_timeout = max(5, min(600, int(params["light_timeout"])))
if "door_timeout" in params:
door_timeout = max(2, min(120, int(params["door_timeout"])))
return True, "自动化规则已保存"
except Exception as e:
return False, "参数无效: %s" % str(e)
def handle_request(conn):
global fallen, fall_source
global shake_hit_count, last_shake_hit_ms, last_local_fall_event_ms
global last_slave_fall_alarm
try:
conn.settimeout(1)
raw = conn.recv(1200)
if not raw:
return
request_line = raw.decode("utf-8", "ignore").split("\r\n", 1)[0]
parts = request_line.split(" ")
if len(parts) < 2:
send_json(conn, {"ok": False, "message": "bad request"}, "400 Bad Request")
return
target = parts[1]
if "?" in target:
path, query = target.split("?", 1)
else:
path, query = target, ""
params = parse_query(query)
if path == "/api/status":
send_json(conn, status_data())
elif path == "/api/control":
ok, message = control_device(params)
data = status_data()
data["ok"] = ok
data["message"] = message
send_json(conn, data, "200 OK" if ok else "400 Bad Request")
elif path == "/api/settings":
ok, message = update_settings(params)
data = status_data()
data["ok"] = ok
data["message"] = message
send_json(conn, data, "200 OK" if ok else "400 Bad Request")
elif path == "/api/reset":
fallen = False
fall_source = ""
shake_hit_count = 0
last_shake_hit_ms = None
last_local_fall_event_ms = time.ticks_ms()
last_slave_fall_alarm = False
udp_send_slave_command({"cmd": "reset_fall"})
data = status_data()
data["message"] = "主机和从机的当前摔倒标记已清除;累计次数保留"
send_json(conn, data)
elif path == "/":
page = (
"<!doctype html><html><head><meta charset='utf-8'>"
"<meta name='viewport' content='width=device-width,initial-scale=1'>"
"<title>智能生活</title></head><body style='font-family:sans-serif;"
"max-width:640px;margin:40px auto;padding:0 16px'>"
"<h1>智能生活主机已启动</h1>"
"<p><a href='/api/status'>查看设备状态</a></p>"
"<p>Wi-Fi模式:%s</p><p>地址:%s</p>"
"</body></html>"
) % (wifi_mode, ip)
page_bytes = page.encode("utf-8")
header = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset=utf-8\r\n"
"Connection: close\r\n"
"Content-Length: %d\r\n\r\n" % len(page_bytes)
)
conn.send(header.encode("utf-8") + page_bytes)
elif path == "/favicon.ico" or parts[0] == "OPTIONS":
send_json(conn, {"ok": True})
else:
send_json(conn, {"ok": False, "message": "not found"}, "404 Not Found")
except Exception as e:
print("HTTP请求处理异常:", e)
finally:
try:
conn.close()
except Exception:
pass
# ==================== Wi-Fi连接:失败后启动备用AP ====================
oled.fill(0)
oled.DispChar("智能生活主机", 0, 0)
oled.DispChar("连接路由WiFi...", 0, 16)
oled.show()
my_wifi = wifi()
try:
my_wifi.connectWiFi(WIFI_SSID, WIFI_PASSWORD, timeout=WIFI_CONNECT_TIMEOUT)
except Exception as e:
print("STA连接异常:", e)
sta_ok = False
try:
sta_ok = my_wifi.sta.isconnected()
except Exception:
sta_ok = False
if sta_ok:
wifi_mode = "STA"
try:
ip = my_wifi.sta.ifconfig()[0]
except Exception:
ip = "0.0.0.0"
else:
wifi_mode = "AP"
oled.fill(0)
oled.DispChar("路由WiFi不可用", 0, 0)
oled.DispChar("启动管理热点...", 0, 16)
oled.show()
try:
my_wifi.enable_APWiFi(essid=AP_SSID, password=AP_PASSWORD, channel=6)
try:
ip = my_wifi.ap.ifconfig()[0]
except Exception:
ip = "192.168.4.1"
except Exception as e:
print("AP启动失败:", e)
ip = "0.0.0.0"
print("智能生活主机模式:", wifi_mode)
print("设备地址: http://" + ip)
if wifi_mode == "AP":
print("连接热点:", AP_SSID, "密码:", AP_PASSWORD)
# ==================== UDP服务:接收从机广播 ====================
try:
slave_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
slave_udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except Exception:
pass
slave_udp.bind(("0.0.0.0", MASTER_UDP_PORT))
slave_udp.setblocking(False)
print("UDP主机已监听端口:", MASTER_UDP_PORT)
except Exception as e:
slave_udp = None
add_warning("UDP主从服务启动失败: %s" % str(e))
# ==================== HTTP服务器 ====================
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 80))
server.listen(3)
server.setblocking(False)
last_oled_ms = 0
last_rfid_ms = 0
# ==================== 主循环 ====================
while True:
now = time.ticks_ms()
# 1) 收从机UDP心率和摔倒状态
poll_slave_udp()
# 2) P0人体感应:触发后同时打开P13/P14,超时后同时关闭
try:
pir_now = bool(pir_pin.read_digital()) if pir_pin is not None else False
except Exception:
pir_now = False
add_warning("P0人体感应读取异常")
if pir_now and pir_auto:
last_motion_ms = now
if not outputs["room_light"]:
set_room_lights(True)
if pir_auto and not pir_now:
if time.ticks_diff(now, last_motion_ms) >= light_timeout * 1000:
if outputs["room_light"]:
set_room_lights(False)
# 3) 每2秒读取SHT20
if time.ticks_diff(now, last_sensor_ms) >= 2000:
last_sensor_ms = now
try:
if sht20 is not None:
temperature = float(sht20.temperature())
humidity = float(sht20.humidity())
sensor_ok = True
else:
sensor_ok = False
except Exception as e:
sensor_ok = False
print("SHT20读取异常:", e)
# 4) 温湿度自动控制风扇:达到任一阈值开启,双低于回差阈值关闭
if (fan_auto and fan_available and sensor_ok and
temperature is not None and humidity is not None):
if not outputs["fan"]:
if temperature >= fan_temp_threshold or humidity >= fan_humidity_threshold:
set_fan(True)
else:
if (temperature <= fan_temp_threshold - 2.0 and
humidity <= fan_humidity_threshold - 5.0):
set_fan(False)
# 5) RFID卡号匹配后点亮P7门灯
if time.ticks_diff(now, last_rfid_ms) >= 250:
last_rfid_ms = now
try:
card = Scan_Rfid.scanning(i2c)
if card:
uid = int(card.serial_number())
if not rfid_card_present:
rfid_last = str(uid)
rfid_authorized = (uid == RFID_AUTHORIZED_UID)
if rfid_authorized:
set_door_light(True)
door_light_deadline = time.ticks_add(now, door_timeout * 1000)
print("RFID授权通过,门灯点亮,UID:", uid)
else:
print("RFID未授权,UID:", uid)
rfid_card_present = True
else:
rfid_card_present = False
except Exception as e:
rfid_available = False
add_warning("RFID读取异常,请检查I2C模块和bluebit驱动")
print("RFID扫描异常:", e)
if door_light_deadline is not None and time.ticks_diff(now, door_light_deadline) >= 0:
set_door_light(False)
door_light_deadline = None
# 6) 本地主机加速度计:摇晃时显示并上报疑似摔倒
detect_shake(now)
# 7) HTTP API请求
try:
conn, addr = server.accept()
handle_request(conn)
except OSError:
pass
except Exception as e:
print("HTTP服务器异常:", e)
# 8) OLED:报警状态优先显示,直到App清除
if time.ticks_diff(now, last_oled_ms) >= 500:
last_oled_ms = now
try:
oled.fill(0)
oled.DispChar("智能生活主机", 0, 0)
oled.DispChar("AP:" + AP_SSID if wifi_mode == "AP" else "IP:" + ip, 0, 16)
if fallen:
oled.DispChar("摔倒报警!!!", 0, 32)
oled.DispChar("请立即确认!", 0, 48)
else:
if sensor_ok and temperature is not None and humidity is not None:
oled.DispChar("T:%.1f H:%.0f%%" % (temperature, humidity), 0, 32)
else:
oled.DispChar("温湿度读取中", 0, 32)
hr_text = "HR:%s*" % (str(heart_rate_bpm) if heart_rate_bpm is not None else "--")
oled.DispChar(hr_text + (" UDP在线" if slave_connected else " UDP等待"), 0, 48)
oled.show()
except Exception:
pass
time.sleep_ms(20)