Files
Competition-2026/onboardprogram/slave.py
T

352 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# 智能生活心率/摔倒监测从机(第二块 mPython 掌控板)
# 主从机都连接 Wi-Fi,通过 UDP 通信;不接 UART 线。
# 心率是模拟数据,不是PPG/ECG真实测量值;摔倒判定为演示级逻辑。
from mpython import *
import socket
import time
import json
import random
import math
# -------------------- Wi-Fi配置 --------------------
# 与主机 main.py 填写相同的路由器 Wi-Fi 账号。
WIFI_SSID = "48-502"
WIFI_PASSWORD = "a13356041516"
# 主机连接路由器失败时会创建此备用AP;从机会自动尝试加入。
AP_SSID = "QHZXManagement"
AP_PASSWORD = "QHZX2026"
WIFI_CONNECT_TIMEOUT = 6
RECONNECT_INTERVAL_MS = 10000
# -------------------- UDP配置 --------------------
MASTER_UDP_PORT = 4210 # 主机接收从机状态广播的端口
SLAVE_UDP_PORT = 4211 # 从机绑定端口,同时接收主机控制命令
SEND_INTERVAL_MS = 500
MASTER_IP_OVERRIDE = "" # 如路由器屏蔽广播,可填写主机IP,例如 192.168.1.20
# -------------------- 模拟心率配置 --------------------
HEART_RATE_MIN = 62
HEART_RATE_MAX = 96
HEART_RATE_UPDATE_MS = 1800
# -------------------- 摔倒演示检测参数 --------------------
ACCEL_SAMPLE_MS = 50
FALL_IMPACT_DELTA_G = 1.35
FALL_TILT_DEGREES = 62
FALL_CONFIRM_MS = 900
FALL_COOLDOWN_MS = 8000
heart_rate_bpm = 74
fall_alarm = False
fall_count = 0
fall_seq = 0
last_hr_update = time.ticks_ms()
last_send = 0
last_accel_sample = 0
last_fall_ms = None
last_accel_magnitude = None
impact_candidate_ms = None
last_button_pressed = False
my_wifi = wifi()
network_ok = False
network_mode = "OFFLINE"
local_ip = "0.0.0.0"
broadcast_ip = "255.255.255.255"
udp_sock = None
last_reconnect_ms = 0
last_network_error = ""
def is_wifi_connected():
try:
return bool(my_wifi.sta.isconnected())
except Exception:
return False
def calc_broadcast(ifconfig):
"""根据本机IP和子网掩码计算定向广播地址。"""
try:
ip_parts = [int(x) for x in ifconfig[0].split(".")]
mask_parts = [int(x) for x in ifconfig[1].split(".")]
result = []
for ip_part, mask_part in zip(ip_parts, mask_parts):
result.append(str(ip_part | (255 - mask_part)))
return ".".join(result)
except Exception:
return "255.255.255.255"
def try_wifi(ssid, password):
global last_network_error
try:
my_wifi.connectWiFi(ssid, password, timeout=WIFI_CONNECT_TIMEOUT)
except Exception as e:
last_network_error = str(e)
print("Wi-Fi连接异常", ssid, e)
return is_wifi_connected()
def close_udp():
global udp_sock
if udp_sock is not None:
try:
udp_sock.close()
except Exception:
pass
udp_sock = None
def setup_udp():
global udp_sock, local_ip, broadcast_ip
try:
info = my_wifi.sta.ifconfig()
local_ip = info[0]
broadcast_ip = calc_broadcast(info)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
except Exception:
pass
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
except Exception:
# 部分 MicroPython 固件不提供 SO_BROADCAST 常量;仍尝试 sendto。
pass
sock.bind(("0.0.0.0", SLAVE_UDP_PORT))
sock.setblocking(False)
udp_sock = sock
print("从机UDP已启动,本机IP:", local_ip)
print("主机UDP广播地址:", broadcast_ip, MASTER_UDP_PORT)
return True
except Exception as e:
print("从机UDP初始化失败:", e)
close_udp()
return False
def connect_network():
"""先连家庭路由器;失败后连主机启动的 QHZXManagement AP。"""
global network_ok, network_mode, local_ip, broadcast_ip
network_ok = False
close_udp()
oled.fill(0)
oled.DispChar("心率监测从机", 0, 0)
oled.DispChar("连接路由WiFi...", 0, 16)
oled.show()
if try_wifi(WIFI_SSID, WIFI_PASSWORD):
network_mode = "STA"
else:
oled.fill(0)
oled.DispChar("路由WiFi不可用", 0, 0)
oled.DispChar("连接主机管理AP", 0, 16)
oled.DispChar(AP_SSID, 0, 32)
oled.show()
# 主机可能刚开始启动AP,分几次尝试加入。
joined = False
for attempt in range(3):
if try_wifi(AP_SSID, AP_PASSWORD):
joined = True
break
time.sleep_ms(1500)
if joined:
network_mode = "AP_CLIENT"
else:
network_mode = "OFFLINE"
network_ok = False
print("家庭Wi-Fi和主机备用AP均连接失败;稍后重试")
return False
try:
local_ip = my_wifi.sta.ifconfig()[0]
broadcast_ip = calc_broadcast(my_wifi.sta.ifconfig())
except Exception:
local_ip = "0.0.0.0"
broadcast_ip = "255.255.255.255"
if not setup_udp():
network_ok = False
return False
network_ok = True
print("从机Wi-Fi模式:", network_mode, "IP:", local_ip)
return True
def send_status():
if not network_ok or udp_sock is None:
return
packet = {
"type": "slave_status",
"transport": "UDP/Wi-Fi",
"heart_rate_bpm": int(heart_rate_bpm),
"heart_rate_simulated": True,
"fall_alarm": bool(fall_alarm),
"fall_count": int(fall_count),
"fall_seq": int(fall_seq),
"fall_source": "slave_accelerometer_demo",
"wifi_mode": network_mode,
"ip_address": local_ip,
"uptime": time.ticks_ms() // 1000,
}
try:
payload = json.dumps(packet).encode("utf-8")
if MASTER_IP_OVERRIDE:
destination_ip = MASTER_IP_OVERRIDE
else:
destination_ip = broadcast_ip
udp_sock.sendto(payload, (destination_ip, MASTER_UDP_PORT))
except Exception as e:
print("UDP发送状态失败:", e)
def poll_master_commands():
global fall_alarm, impact_candidate_ms
if udp_sock is None:
return
for _ in range(4):
try:
data, addr = udp_sock.recvfrom(512)
except OSError:
break
except Exception as e:
print("UDP接收异常:", e)
break
try:
command = json.loads(data.decode("utf-8", "ignore"))
if command.get("cmd") == "reset_fall":
fall_alarm = False
impact_candidate_ms = None
print("收到主机UDP指令:清除当前摔倒报警")
send_status()
except Exception as e:
print("主机UDP命令解析异常:", e, "来源:", addr)
def simulate_heart_rate(now):
global heart_rate_bpm, last_hr_update
if time.ticks_diff(now, last_hr_update) >= HEART_RATE_UPDATE_MS:
last_hr_update = now
# 缓慢随机变化,只用于界面联调和演示。
heart_rate_bpm += random.randint(-3, 3)
heart_rate_bpm = max(HEART_RATE_MIN, min(HEART_RATE_MAX, heart_rate_bpm))
def trigger_fall(source="accelerometer_demo"):
global fall_alarm, fall_count, fall_seq, last_fall_ms, impact_candidate_ms
now = time.ticks_ms()
if last_fall_ms is not None and time.ticks_diff(now, last_fall_ms) < FALL_COOLDOWN_MS:
return
fall_alarm = True
fall_count += 1
fall_seq += 1
last_fall_ms = now
impact_candidate_ms = None
print("疑似摔倒报警,从机来源:", source, "累计:", fall_count)
send_status()
def check_fall(now):
global last_accel_magnitude, impact_candidate_ms
try:
x = accelerometer.get_x()
y = accelerometer.get_y()
z = accelerometer.get_z()
magnitude = math.sqrt(x * x + y * y + z * z)
if last_accel_magnitude is not None:
delta = abs(magnitude - last_accel_magnitude)
if delta >= FALL_IMPACT_DELTA_G:
impact_candidate_ms = now
last_accel_magnitude = magnitude
if impact_candidate_ms is not None:
elapsed = time.ticks_diff(now, impact_candidate_ms)
if elapsed >= FALL_CONFIRM_MS:
try:
roll, pitch = accelerometer.roll_pitch_angle()
tilted = (
abs(roll) >= FALL_TILT_DEGREES
or abs(pitch) >= FALL_TILT_DEGREES
)
except Exception:
tilted = False
if tilted:
trigger_fall("accelerometer_impact_and_tilt")
elif elapsed >= 2000:
impact_candidate_ms = None
except Exception as e:
print("加速度计读取异常:", e)
def test_button():
global last_button_pressed
try:
pressed = button_a.value() == 0
except Exception:
pressed = False
if pressed and not last_button_pressed:
trigger_fall("button_a_test")
last_button_pressed = pressed
# -------------------- 启动与主循环 --------------------
connect_network()
last_reconnect_ms = time.ticks_ms()
while True:
now = time.ticks_ms()
# Wi-Fi中断时重新尝试:家庭路由器优先,其次主机备用AP。
if not is_wifi_connected():
network_ok = False
close_udp()
if not network_ok and time.ticks_diff(now, last_reconnect_ms) >= RECONNECT_INTERVAL_MS:
last_reconnect_ms = now
connect_network()
poll_master_commands()
simulate_heart_rate(now)
if time.ticks_diff(now, last_accel_sample) >= ACCEL_SAMPLE_MS:
last_accel_sample = now
check_fall(now)
test_button()
if time.ticks_diff(now, last_send) >= SEND_INTERVAL_MS:
last_send = now
send_status()
try:
oled.fill(0)
oled.DispChar("心率从机 UDP", 0, 0)
oled.DispChar("HR:%d BPM 模拟" % heart_rate_bpm, 0, 16)
oled.DispChar("摔倒报警!" if fall_alarm else "状态正常", 0, 32)
oled.DispChar("STA:%s" % local_ip if network_ok else "WiFi重连中", 0, 48)
oled.show()
except Exception:
pass
time.sleep_ms(20)