|
8| 0
|
[Micropython] C4002 毫米波雷达——老人卫生间长时间静止报警系统 |
|
基于 MicroPython 与 DFRobot SEN0691 C4002 毫米波雷达的人体静止报警项目报告一、项目名称 老人卫生间长时间静止报警系统 二、真实痛点与应用背景老人独居或夜间如厕时,卫生间是跌倒高风险场景。传统 PIR 红外人体传感器只能对明显肢体运动敏感,当老人跌倒后长时间静止、坐在地面微动、或因眩晕无法主动呼救时,PIR 很容易误判为“无人”。摄像头虽然可识别姿态,但卫生间属于强隐私空间,不适合部署图像采集设备。 本项目使用 DFRobot SEN0691 C4002 mmWave Motion and Static Presence Module,通过毫米波雷达检测“运动人体”和“静止/微动人体”,并结合 ESP32 MicroPython 实现本地报警。系统不拍摄图像,不上传人体画面,适合卫生间、卧室、养老公寓等隐私敏感区域。 三、项目目标本项目解决的核心问题是:当卫生间内检测到有人,但连续较长时间只有静止/微动、无明显运动时,系统先发出预警提示;若无人按键取消,则触发蜂鸣器和继电器报警。 设计目标如下: 1. 能识别“无人、有人静止/微动、有人运动”三类状态。 2. 避免 PIR 对静止人体漏检的问题。 3. 限定检测距离,减少卫生间门外、走廊、隔墙人体造成的误触发。 4. 设置“预报警—正式报警”两级机制,避免老人正常久坐时直接惊扰。 5. 通过取消按钮手动解除报警,体现真实场景可用性。 6. 全部核心逻辑在 ESP32 本地运行,断网也能报警。 四、核心硬件
注意:如果继电器控制 220V 市电灯具,必须由具备电工资质人员处理,弱电系统与强电必须隔离。课堂或原型验证建议只接低压蜂鸣器或 12V 警示灯。 五、传感器能力说明C4002 是基于 24GHz FMCW 技术的毫米波人体存在传感器。与传统 PIR 相比,它不仅能检测人走动时的大幅运动,也能检测人体静止时由呼吸、轻微晃动造成的微动信号。模块通过 UART 输出目标状态、存在距离、运动距离、运动速度、运动方向、目标能量和环境光照等数据。 本项目使用 UART 模式,不只读取 OUT 高低电平,因为 UART 能得到更丰富的数据,例如: · target:近、远离或无方向。无人、静止/微动、运动; · presence_distance_m:静止/微动目标距离; · motion_distance_m:运动目标距离; · presence_energy / motion_energy:目标能量; · light_lux:环境光照; · motion_speed_mps:运动速度; · motion_direction:靠 这些信息可用于过滤门外人员、排除弱干扰、判断是否长时间静止。 六、安装位置与检测区域推荐安装方式: · 卫生间面积:约 1.5m × 2.5m; · 传感器安装在门内侧上方或侧墙,高度约 1.8–2.2m; · 雷达正面朝向马桶、淋浴区与地面活动区域; · 不建议正对金属门、大面积镜子或强反射不锈钢置物架; · 不建议直接贴近换气扇、浴帘、晾衣杆等频繁摆动目标。 本项目建议检测距离设为 40–240cm。这样可以覆盖卫生间内部人员,又能减少门外走廊人员被毫米波穿透或旁瓣检测到的概率。 七、接线说明7.1 C4002 与 ESP32 接线表
┌──────────────────────┐ 八、系统逻辑│ ESP32 DevKit │ │ │ 5V ─────────────┤ 5V/VIN │ GND ─────────────┤ GND │ │ │ GPIO17 / TX1 ────┤ UART1 TX │ GPIO16 / RX1 ────┤ UART1 RX │ GPIO27 ◄────────┤ 可选 OUT 输入 │ │ │ GPIO25 ─────────►│ 蜂鸣器 IN │ GPIO26 ─────────►│ 继电器 IN │ GPIO2 ─────────►│ LED + 220Ω │ GPIO14 ◄─────────┤ 取消按钮,另一端接 GND │ └──────────────────────┘ ▲ ▲ │ │ │ │ ┌───────┴───────┴──────┐ │ DFRobot C4002 │ │ │ │ VIN ◄──── 5V │ │ GND ◄──── GND │ │ RX ◄──── ESP32 TX17 │ │ TX ────► ESP32 RX16 │ │ OUT ────► ESP32 GPIO27,可选 │ └───────────────────────┘ 系统将卫生间状态分为五个状态: 1. IDLE:无人 2. ACTIVE:有人且有运动 3. STATIC_WATCH:有人静止/微动,开始计时 4. PREALARM:超过静止阈值,短促蜂鸣提醒 5. ALARM:预报警期间无人取消,继电器与蜂鸣器正式报警 核心判断逻辑: · 如果 C4002 输出 MOTION,说明有明显运动,重置静止计时。 · 如果 C4002 输出 PRESENCE,说明检测到静止/微动人体。 · 若持续 PRESENCE 且超过 8 分钟没有 MOTION,进入预报警。 · 预报警持续 30 秒,蜂鸣器间歇响,提醒老人按下取消按钮。 · 若 30 秒内按下取消按钮,则静默 10 分钟。 · 若无人按键取消,则进入正式报警,继电器和蜂鸣器启动。 · 如果检测为 NO_TARGET,说明卫生间无人,所有报警状态复位。 九、流程图 十、完整 MicroPython 代码文件名建议保存为 main.py,上传到 ESP32 后复位运行。 # main.py 十一、代码逻辑说明11.1 UART 协议解析# ESP32 MicroPython + DFRobot SEN0691 C4002 # 项目:老人卫生间长时间静止报警系统 # # UART 接线: # C4002 TX -> ESP32 GPIO16 / UART1 RX # C4002 RX -> ESP32 GPIO17 / UART1 TX # C4002 VIN -> 5V # C4002 GND -> GND # # 外设: # GPIO25 -> 有源蜂鸣器 IN,高电平响 # GPIO26 -> 继电器 IN,高电平触发 # GPIO2 -> LED # GPIO14 -> 取消按钮,另一端接 GND,内部上拉 from machine import UART, Pin import time class C4002: # Frame headers HEADER = b"\xFA\xF5\xAA\xA5" # Frame type FRAME_TYPE_WRITE_REQUEST = 0x00 FRAME_TYPE_READ_REQUEST = 0x01 FRAME_TYPE_WRITE_RESPOND = 0x02 FRAME_TYPE_READ_RESPOND = 0x03 FRAME_TYPE_NOTIFICATION = 0x04 # Commands CMD_SET_LED_MODE = 0xA1 CMD_CONFIG_OUT_MODE = 0xA0 CMD_ENV_CALIBRATION = 0x60 CMD_SET_DETECT_RANGE = 0x86 CMD_SET_REPORT_PERIOD = 0x83 CMD_SET_LIGHT_THRESHOLD = 0x88 CMD_SET_DISTANCE_GATE = 0x62 CMD_GET_SET_RESOLUTION_MODE = 0x66 CMD_TARGET_DISAPPEAR_DELAY = 0x84 CMD_LOCK_TIME = 0x85 CMD_THRESHOLD_GROUP = 0x87 # Notification command NOTE_RESULT_CMD = 0x60 NOTE_ENV_CALIBRATION_CMD = 0x03 # Response code SUCCEED = 0x01 # Target state NO_TARGET = 0 PRESENCE = 1 MOTION = 2 # Direction AWAY = 0 NO_DIRECTION = 1 APPROACHING = 2 # Resolution RESOLUTION_80CM = 0x00 RESOLUTION_20CM = 0x01 # Distance gate type MOTION_DISTANCE_GATE = 0x00 PRESENCE_DISTANCE_GATE = 0x01 # Sensitivity group LOW_THRESH_GROUP = 0x00 MID_THRESH_GROUP = 0x01 HIGH_THRESH_GROUP = 0x02 CUSTOM_THRESH_GROUP = 0x03 # LED LED_OFF = 0x00 LED_ON = 0x01 LED_KEEP = 0xFF # OUT pin mode OUT_PIN_MODE_MOTION_ONLY = 0x01 OUT_PIN_MODE_PRESENCE_ONLY = 0x02 OUT_PIN_MODE_MOTION_OR_PRESENCE = 0x03 def __init__(self, uart_id=1, baudrate=115200, tx_pin=17, rx_pin=16): self.uart = UART( uart_id, baudrate=baudrate, bits=8, parity=None, stop=1, tx=Pin(tx_pin), rx=Pin(rx_pin), timeout=0, timeout_char=0 ) self.resolution_mode = self.RESOLUTION_80CM self.last_result = None @staticmethod def _u16_le(buf, pos): return buf[pos] | (buf[pos + 1] << 8) @staticmethod def _u32_le(buf, pos): return ( buf[pos] | (buf[pos + 1] << 8) | (buf[pos + 2] << 16) | (buf[pos + 3] << 24) ) @staticmethod def _i16_le(buf, pos): value = buf[pos] | (buf[pos + 1] << 8) if value & 0x8000: value -= 0x10000 return value def _read_exact(self, n, timeout_ms=300): data = bytearray() deadline = time.ticks_add(time.ticks_ms(), timeout_ms) while len(data) < n and time.ticks_diff(deadline, time.ticks_ms()) > 0: chunk = self.uart.read(n - len(data)) if chunk: data.extend(chunk) else: time.sleep_ms(2) if len(data) == n: return bytes(data) return None def _read_frame(self, timeout_ms=500): # 1. Find header matched = 0 deadline = time.ticks_add(time.ticks_ms(), timeout_ms) while time.ticks_diff(deadline, time.ticks_ms()) > 0: b = self.uart.read(1) if not b: time.sleep_ms(2) continue value = b[0] if value == self.HEADER[matched]: matched += 1 if matched == 4: break else: matched = 1 if value == self.HEADER[0] else 0 if matched != 4: return None # 2. Read length low/high, reserved byte, frame type rest = self._read_exact(4, timeout_ms=timeout_ms) if rest is None: return None packet_len = rest[0] | (rest[1] << 8) frame_type = rest[3] # Basic sanity check. C4002 frames are short; 128 is enough here. if packet_len < 12 or packet_len > 128: return None # 3. Read remaining frame body, including payload and checksum remaining = self._read_exact(packet_len - 8, timeout_ms=timeout_ms) if remaining is None: return None frame = self.HEADER + rest + remaining rx_sum = frame[-2] | (frame[-1] << 8) calc_sum = sum(frame[:-2]) & 0xFFFF if rx_sum != calc_sum: return None payload = frame[8:-2] if len(payload) < 4: return None cmd = payload[0] resp_code = payload[1] inner_len = payload[2] | (payload[3] << 8) # inner_len includes cmd, resp_code, lenL, lenH data = payload[4:inner_len] return { "frame_type": frame_type, "cmd": cmd, "resp_code": resp_code, "data": data, } def _send_frame(self, cmd, params=b"", frame_type=FRAME_TYPE_WRITE_REQUEST): inner_len = 4 + len(params) payload = bytearray() payload.append(cmd) payload.append(0x00) # READ_AND_WRITE_REQ payload.append(inner_len & 0xFF) payload.append((inner_len >> 8) & 0xFF) payload.extend(params) packet_len = len(payload) + 10 frame = bytearray() frame.extend(self.HEADER) frame.append(packet_len & 0xFF) frame.append((packet_len >> 8) & 0xFF) frame.append(0x00) frame.append(frame_type) frame.extend(payload) checksum = sum(frame) & 0xFFFF frame.append(checksum & 0xFF) frame.append((checksum >> 8) & 0xFF) # Clear old bytes before sending config/read command while self.uart.any(): self.uart.read() self.uart.write(frame) def _command(self, cmd, params=b"", read=False, timeout_ms=600): frame_type = self.FRAME_TYPE_READ_REQUEST if read else self.FRAME_TYPE_WRITE_REQUEST expect_type = self.FRAME_TYPE_READ_RESPOND if read else self.FRAME_TYPE_WRITE_RESPOND self._send_frame(cmd, params=params, frame_type=frame_type) deadline = time.ticks_add(time.ticks_ms(), timeout_ms) while time.ticks_diff(deadline, time.ticks_ms()) > 0: remain = time.ticks_diff(deadline, time.ticks_ms()) frame = self._read_frame(timeout_ms=remain if remain > 20 else 20) if not frame: continue if frame["cmd"] == cmd and frame["frame_type"] == expect_type: return frame["resp_code"] == self.SUCCEED, frame return False, None def set_report_period(self, period): # unit: 0.1 s. 10 means 1 s. period = max(0, min(255, int(period))) return self._command( self.CMD_SET_REPORT_PERIOD, params=bytes([period]), read=False )[0] def set_resolution_mode(self, mode): ok, _ = self._command( self.CMD_GET_SET_RESOLUTION_MODE, params=bytes([mode]), read=False ) if ok: self.resolution_mode = mode return ok def set_detect_range(self, closest_cm, farthest_cm): closest_cm = max(0, int(closest_cm)) farthest_cm = min(1100, int(farthest_cm)) if closest_cm > farthest_cm: return False params = bytes([ closest_cm & 0xFF, (closest_cm >> 8) & 0xFF, farthest_cm & 0xFF, (farthest_cm >> 8) & 0xFF ]) return self._command(self.CMD_SET_DETECT_RANGE, params=params, read=False)[0] def configure_gate(self, gate_type, gate_data): gate_num = 15 if self.resolution_mode == self.RESOLUTION_80CM else 25 if len(gate_data) != gate_num: raise ValueError("gate_data length must be %d" % gate_num) params = bytearray() params.append(gate_type) params.extend([1 if x else 0 for x in gate_data]) return self._command(self.CMD_SET_DISTANCE_GATE, params=bytes(params), read=False)[0] def set_light_threshold(self, lux): # 0 means detection is not limited by light level lux = max(0, min(50, float(lux))) v = int(lux * 10) params = bytes([v & 0xFF, (v >> 8) & 0xFF]) return self._command(self.CMD_SET_LIGHT_THRESHOLD, params=params, read=False)[0] def set_target_disappear_delay(self, seconds): seconds = max(0, min(65535, int(seconds))) params = bytes([seconds & 0xFF, (seconds >> 8) & 0xFF]) return self._command(self.CMD_TARGET_DISAPPEAR_DELAY, params=params, read=False)[0] def set_lock_time(self, seconds): # range normally 0.2–10.0 s, unit 0.1 s seconds = max(0.2, min(10.0, float(seconds))) v = int(seconds * 10) params = bytes([v & 0xFF, (v >> 8) & 0xFF]) return self._command(self.CMD_LOCK_TIME, params=params, read=False)[0] def set_sensitivity(self, gate_type, sensitivity_group): params = bytes([gate_type, sensitivity_group]) return self._command(self.CMD_THRESHOLD_GROUP, params=params, read=False)[0] def set_out_pin_mode(self, mode): return self._command(self.CMD_CONFIG_OUT_MODE, params=bytes([mode]), read=False)[0] def set_run_led_state(self, state): params = bytes([state, self.LED_KEEP]) return self._command(self.CMD_SET_LED_MODE, params=params, read=False)[0] def set_out_led_state(self, state): params = bytes([self.LED_KEEP, state]) return self._command(self.CMD_SET_LED_MODE, params=params, read=False)[0] def start_env_calibration(self, delay_s=10, calibration_s=30): # 调试时使用。正式运行时不要每次上电都自动校准,避免有人在场时校准出错。 params = bytes([ delay_s & 0xFF, (delay_s >> 8) & 0xFF, calibration_s & 0xFF, (calibration_s >> 8) & 0xFF, 0x01 ]) self._command(self.CMD_ENV_CALIBRATION, params=params, read=False, timeout_ms=1000) def read_notification(self, timeout_ms=300): frame = self._read_frame(timeout_ms=timeout_ms) if not frame: return None if frame["frame_type"] != self.FRAME_TYPE_NOTIFICATION: return None if frame["resp_code"] != self.SUCCEED: return None cmd = frame["cmd"] data = frame["data"] if cmd == self.NOTE_RESULT_CMD: if len(data) < 18: return None target = data[0] light_lux = self._u16_le(data, 1) * 0.1 result = { "type": "result", "target": target, "target_name": self.target_name(target), "light_lux": light_lux, "presence_gate_index": self._u32_le(data, 3), "presence_countdown_s": self._u16_le(data, 7), "presence_distance_m": self._u16_le(data, 9) * 0.01, "presence_energy": data[11], "motion_distance_m": self._u16_le(data, 12) * 0.01, "motion_speed_mps": self._i16_le(data, 14) * 0.01, "motion_energy": data[16], "motion_direction": data[17], "motion_direction_name": self.direction_name(data[17]), } self.last_result = result return result if cmd == self.NOTE_ENV_CALIBRATION_CMD and len(data) >= 2: return { "type": "calibration", "calibration_countdown_s": self._u16_le(data, 0) } return None def target_name(self, target): if target == self.NO_TARGET: return "NO_TARGET" if target == self.PRESENCE: return "PRESENCE_STATIC" if target == self.MOTION: return "MOTION" return "UNKNOWN" def direction_name(self, direction): if direction == self.AWAY: return "AWAY" if direction == self.NO_DIRECTION: return "NO_DIRECTION" if direction == self.APPROACHING: return "APPROACHING" return "UNKNOWN" # ========== 用户可调参数 ========== UART_ID = 1 UART_TX_PIN = 17 UART_RX_PIN = 16 UART_BAUD = 115200 LED_PIN = 2 BUZZER_PIN = 25 RELAY_PIN = 26 CANCEL_BUTTON_PIN = 14 # 继电器触发电平。大多数模块高电平触发;若你的模块低电平触发,改为 0。 RELAY_ACTIVE_LEVEL = 1 # 卫生间有效检测距离,单位 m VALID_MIN_DISTANCE_M = 0.40 VALID_MAX_DISTANCE_M = 2.40 # 静止多久进入预报警。演示可改成 30;真实项目建议 6~12 分钟。 STATIC_ALARM_SECONDS = 8 * 60 # 预报警持续时间。期间老人可按取消按钮。 PREALARM_SECONDS = 30 # 按下取消后静默时间,避免正常久坐反复报警。 CANCEL_MUTE_SECONDS = 10 * 60 # 能量下限。过低能量可能来自边缘、门外或弱干扰;不同卫生间需实测调整。 MIN_PRESENCE_ENERGY = 5 MIN_MOTION_ENERGY = 5 # C4002 上报周期:10 × 0.1s = 1s REPORT_PERIOD = 10 # ========== GPIO 初始化 ========== led = Pin(LED_PIN, Pin.OUT) buzzer = Pin(BUZZER_PIN, Pin.OUT) relay = Pin(RELAY_PIN, Pin.OUT) cancel_btn = Pin(CANCEL_BUTTON_PIN, Pin.IN, Pin.PULL_UP) def relay_on(): relay.value(RELAY_ACTIVE_LEVEL) def relay_off(): relay.value(0 if RELAY_ACTIVE_LEVEL else 1) def all_alarm_off(): buzzer.value(0) relay_off() def is_button_pressed(): # 简单消抖 if cancel_btn.value() == 0: time.sleep_ms(20) return cancel_btn.value() == 0 return False def beep_prealarm(now_ms): # 慢速间歇蜂鸣:0.5s 响 / 0.5s 停 buzzer.value(1 if (now_ms // 500) % 2 == 0 else 0) def beep_alarm(now_ms): # 快速蜂鸣 buzzer.value(1 if (now_ms // 150) % 2 == 0 else 0) def in_valid_zone(info): if info["target"] == C4002.MOTION: distance = info["motion_distance_m"] energy = info["motion_energy"] return ( VALID_MIN_DISTANCE_M <= distance <= VALID_MAX_DISTANCE_M and energy >= MIN_MOTION_ENERGY ) if info["target"] == C4002.PRESENCE: distance = info["presence_distance_m"] energy = info["presence_energy"] return ( VALID_MIN_DISTANCE_M <= distance <= VALID_MAX_DISTANCE_M and energy >= MIN_PRESENCE_ENERGY ) return False # ========== C4002 初始化 ========== radar = C4002( uart_id=UART_ID, baudrate=UART_BAUD, tx_pin=UART_TX_PIN, rx_pin=UART_RX_PIN ) print("Booting C4002 elderly bathroom static alarm...") # 上电稳定 time.sleep_ms(800) # 先尝试配置;若失败,继续重试,便于调试接线。 while not radar.set_report_period(REPORT_PERIOD): print("C4002 set_report_period failed, check UART wiring...") time.sleep(1) # 小卫生间使用 20cm 分辨率,有利于更精细地限制区域。 if radar.set_resolution_mode(C4002.RESOLUTION_20CM): print("Resolution: 20cm") else: print("Set resolution failed; continue with current mode.") # 有效距离限制:40cm ~ 240cm。 if radar.set_detect_range( int(VALID_MIN_DISTANCE_M * 100), int(VALID_MAX_DISTANCE_M * 100) ): print("Detect range configured.") else: print("Set detect range failed.") # 光照阈值设为 0,表示不因亮度禁止人体检测。 radar.set_light_threshold(0) # 目标消失延时设为 3s,避免短时丢帧导致状态抖动。 radar.set_target_disappear_delay(3) # 锁定时间略短,减少“无人到有人”状态切换后的盲区。 radar.set_lock_time(0.5) # OUT 引脚配置为运动或存在均输出。项目主逻辑使用 UART,这里保留给外部联动。 radar.set_out_pin_mode(C4002.OUT_PIN_MODE_MOTION_OR_PRESENCE) # 关闭模块板载 LED,减少夜间卫生间光污染;调试时可改为 LED_ON。 radar.set_run_led_state(C4002.LED_OFF) radar.set_out_led_state(C4002.LED_OFF) # 启用全部 20cm 距离门。若要屏蔽门外区域,可把某些 gate 改为 0。 gate_20cm_all_enabled = [1] * 25 radar.configure_gate(C4002.MOTION_DISTANCE_GATE, gate_20cm_all_enabled) radar.configure_gate(C4002.PRESENCE_DISTANCE_GATE, gate_20cm_all_enabled) # 环境底噪采集: # 首次安装调试时可手动取消下面注释,运行一次。 # 启动后 10s 内离开卫生间,等待 30s 完成环境采集。 # 完成后建议重新注释,避免每次上电有人在场时采集错误。 # radar.start_env_calibration(delay_s=10, calibration_s=30) print("System ready.") # ========== 状态机 ========== STATE_IDLE = "IDLE" STATE_ACTIVE = "ACTIVE" STATE_STATIC_WATCH = "STATIC_WATCH" STATE_PREALARM = "PREALARM" STATE_ALARM = "ALARM" STATE_CANCELLED = "CANCELLED" state = STATE_IDLE presence_since_ms = None last_motion_ms = None prealarm_since_ms = None cancel_mute_until_ms = 0 last_debug_print_ms = 0 def reset_to_idle(): global state, presence_since_ms, last_motion_ms, prealarm_since_ms state = STATE_IDLE presence_since_ms = None last_motion_ms = None prealarm_since_ms = None led.value(0) all_alarm_off() while True: now = time.ticks_ms() # 取消按钮优先级最高 if is_button_pressed(): cancel_mute_until_ms = time.ticks_add(now, CANCEL_MUTE_SECONDS * 1000) state = STATE_CANCELLED prealarm_since_ms = None last_motion_ms = now all_alarm_off() led.value(1) print("Alarm cancelled. Muted for %d seconds." % CANCEL_MUTE_SECONDS) # 等待松手,避免重复触发 while cancel_btn.value() == 0: time.sleep_ms(20) info = radar.read_notification(timeout_ms=150) if info and info.get("type") == "calibration": print("Calibration countdown:", info["calibration_countdown_s"], "s") if info and info.get("type") == "result": target = info["target"] target_name = info["target_name"] valid = in_valid_zone(info) # 调试输出,每 2s 打印一次 if time.ticks_diff(now, last_debug_print_ms) > 2000: last_debug_print_ms = now print( "state=%s target=%s valid=%s light=%.1f " "p_dist=%.2fm p_energy=%d m_dist=%.2fm m_energy=%d speed=%.2fm/s dir=%s" % ( state, target_name, valid, info["light_lux"], info["presence_distance_m"], info["presence_energy"], info["motion_distance_m"], info["motion_energy"], info["motion_speed_mps"], info["motion_direction_name"], ) ) if target == C4002.NO_TARGET or not valid: reset_to_idle() elif target == C4002.MOTION: # 检测到明显运动:说明老人仍可活动,重置静止计时。 presence_since_ms = now if presence_since_ms is None else presence_since_ms last_motion_ms = now prealarm_since_ms = None state = STATE_ACTIVE led.value(1) all_alarm_off() elif target == C4002.PRESENCE: # 检测到静止/微动人体 if presence_since_ms is None: presence_since_ms = now if last_motion_ms is None: # 第一次进入即静止,按“进入时间”开始计时 last_motion_ms = presence_since_ms led.value(1) in_cancel_window = time.ticks_diff(cancel_mute_until_ms, now) > 0 static_seconds = time.ticks_diff(now, last_motion_ms) // 1000 if in_cancel_window: state = STATE_CANCELLED all_alarm_off() elif static_seconds >= STATIC_ALARM_SECONDS: if state not in (STATE_PREALARM, STATE_ALARM): state = STATE_PREALARM prealarm_since_ms = now print("PREALARM: static presence too long.") else: state = STATE_STATIC_WATCH all_alarm_off() # 根据状态驱动报警器 now = time.ticks_ms() if state == STATE_PREALARM: beep_prealarm(now) relay_off() if prealarm_since_ms is not None: prealarm_elapsed = time.ticks_diff(now, prealarm_since_ms) // 1000 if prealarm_elapsed >= PREALARM_SECONDS: state = STATE_ALARM print("ALARM: no cancellation during prealarm.") elif state == STATE_ALARM: beep_alarm(now) relay_on() led.value(1) elif state in (STATE_IDLE, STATE_ACTIVE, STATE_STATIC_WATCH, STATE_CANCELLED): # ACTIVE / STATIC_WATCH 时 LED 表示有人,蜂鸣器和继电器关闭 if state == STATE_IDLE: led.value(0) all_alarm_off() time.sleep_ms(20) C4002 的 UART 数据帧以 FA F5 AA A5 作为帧头。ESP32 先搜索帧头,再读取长度、帧类型、命令、响应码、数据段和校验和。 本项目代码实现了: · _read_frame():从 UART 中读取并校验完整帧; · _send_frame():按照 C4002 协议封装配置命令; · _command():发送写/读命令并等待响应; · read_notification():解析传感器主动上报结果; · set_report_period():设置上报周期; · set_resolution_mode():设置 80cm 或 20cm 距离分辨率; · set_detect_range():设置最近/最远检测距离; · configure_gate():启用/关闭距离门; · set_target_disappear_delay():设置目标消失延时; · set_light_threshold():设置光照阈值。 11.2 状态机逻辑运行时主循环持续读取 C4002 通知帧: · NO_TARGET:无人,关闭蜂鸣器和继电器。 · MOTION:有人且在运动,更新 last_motion_ms。 · PRESENCE:有人但只有静止/微动,开始计算距离上次运动的时间。 · 超过 STATIC_ALARM_SECONDS:进入预报警。 · 预报警超过 PREALARM_SECONDS 且无人按键:进入正式报警。 · 按下取消按钮:进入静默窗口,避免正常久坐反复报警。 十二、关键参数设置建议
PIR 依赖人体热源运动变化,老人跌倒后如果长时间不动,PIR 可能无法持续输出“有人”。C4002 可检测静止/微动人体,更适合“人在但不动”的场景。本项目正是利用 PRESENCE 状态作为长时间静止判断依据。 难点二:卫生间隐私场景不能使用摄像头跌倒检测常见方案会用摄像头或姿态识别,但卫生间不适合采集图像。本项目只使用毫米波状态、距离、能量等非图像信息,在隐私保护方面更合理。 难点三:卫生间空间小,门外人员可能误触发毫米波具有一定穿透能力,且 120° 视场较宽,如果不限制距离,门外走廊人员可能被识别。本项目通过三层过滤降低误报: 1. set_detect_range(40, 240) 限定检测距离; 2. in_valid_zone() 对 presence/motion 距离再次过滤; 3. 目标能量低于阈值时不进入报警逻辑。 难点四:浴帘、换气扇、门摆动等动态干扰卫生间内可能存在浴帘晃动、换气扇震动、门缝气流等干扰。处理方法: · 安装时不要让雷达正对浴帘或风扇; · 首次安装时进行环境底噪采集; · 若某些距离门长期误报,可将对应 gate 关闭; · 对目标能量设置下限; · 报警逻辑不因瞬时 presence 触发,而要求持续静止超过阈值。 难点五:长时间静止不一定等于跌倒老人正常如厕、洗澡休息也可能长时间静止。因此本项目不直接声称“跌倒识别”,而是设计为“长时间静止异常报警”。同时加入预报警和取消按钮: · 先短促蜂鸣提醒; · 老人可按键取消; · 无人取消才进入正式报警。 这比直接报警更符合真实家庭使用。 难点六:MicroPython 下没有直接可用 Arduino 库DFRobot 官方库主要面向 Arduino 和 Raspberry Pi Python。ESP32 MicroPython 无法直接调用 Arduino C++ 库,因此本项目手动实现 UART 帧封装、校验和通知解析。代码中完整实现了 C4002 命令发送、响应校验和目标数据解析。 十四、测试方案14.1 单人进入测试步骤: 1. 老人或测试者进入卫生间; 2. 雷达应输出 MOTION; 3. LED 点亮; 4. 蜂鸣器与继电器不动作。 预期结果:系统进入 ACTIVE 状态。 14.2 静坐测试步骤: 1. 测试者坐在马桶或凳子上保持静止; 2. 串口输出应逐渐保持 PRESENCE_STATIC; 3. 等待超过 STATIC_ALARM_SECONDS; 4. 系统进入 PREALARM。 预期结果:蜂鸣器慢速间歇响,继电器不动作。 14.3 取消按钮测试步骤: 1. 进入预报警后按下取消按钮; 2. 系统停止蜂鸣; 3. 10 分钟内不再重复报警。 预期结果:系统进入 CANCELLED,避免正常如厕误扰。 14.4 正式报警测试步骤: 1. 将演示参数 STATIC_ALARM_SECONDS 改为 30; 2. 静止 30 秒进入预报警; 3. 30 秒内不按取消按钮。 预期结果:系统进入 ALARM,蜂鸣器快速鸣叫,继电器吸合。 14.5 门外干扰测试步骤: 1. 卫生间无人; 2. 让测试者在门外走动; 3. 观察串口距离和能量; 4. 调整 VALID_MAX_DISTANCE_M 或距离门。 预期结果:门外人员不触发 PREALARM 或 ALARM。 十五、改进方向1. 联网通知:增加 Wi-Fi 与 MQTT,将报警发送到 Home Assistant、微信机器人或家庭网关。 2. 双传感器交叉验证:在较复杂卫生间中增加门磁或第二个 C4002,区分门外与室内。 3. 夜间灯光联动:检测到夜间有人进入时自动打开低亮度灯带。 4. 分区判断:根据距离门将“马桶区、淋浴区、门口区”分开,针对不同区域设置不同静止阈值。 5. 低功耗与掉电恢复:增加看门狗、掉电记录、报警日志存储。 6. 更柔和的人机交互:预报警阶段可先用语音提示:“请按按钮确认安全”。 十六、结论本项目基于 ESP32 MicroPython 与 DFRobot SEN0691 C4002 毫米波雷达,实现了卫生间长时间静止异常报警。方案避免了摄像头带来的隐私问题,也弥补了 PIR 传感器无法可靠检测静止人体的缺陷。通过距离范围限制、能量阈值、环境底噪采集、两级报警和取消按钮,系统具备真实家庭场景落地价值。 该项目不应被宣传为医学级跌倒诊断设备,而应定位为“隐私友好的长时间静止异常提醒系统”。在养老看护、独居老人安全、夜间卫生间监测等场景中,它能以低成本提供及时提醒,降低无人发现的风险。 |
沪公网安备31011502402448© 2013-2026 Comsenz Inc. Powered by Discuz! X3.4 Licensed