# -*- 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 = ( "" "" "智能生活" "

智能生活主机已启动

" "

查看设备状态

" "

Wi-Fi模式:%s

地址:%s

" "" ) % (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)