소스 검색

睡眠监测算法

nifangxu 1 주 전
부모
커밋
65709d65ec
5개의 변경된 파일248개의 추가작업 그리고 15개의 파일을 삭제
  1. 203 1
      core/alarm_plan.py
  2. 12 0
      core/alarm_plan_helper.py
  3. 12 8
      core/event_type.py
  4. 11 1
      device/dev_mng.py
  5. 10 5
      mqtt/mqtt_recv.py

+ 203 - 1
core/alarm_plan.py

@@ -6,6 +6,7 @@ import uuid
 import json
 import traceback
 from datetime import datetime, timezone, timedelta
+from collections import deque
 
 import common.sys_comm as sys_comm
 from common.sys_comm import (
@@ -161,6 +162,35 @@ class EventAttr_TargetAbsence(EventAttr_Base):
         self.enter_ts_  = -1
         self.absence_time_ = -1
 
+# 事件属性 睡眠监测
+class EventAttr_SleepMonitoring(EventAttr_Base):
+    motion_stat = ["peaceful", "micro", "active", "leave"]  # 空间状态
+    # 呼吸率划分:r0:[0-8) r1:[8,12) r2:[12,18) r3:[18,25] r4:25以上
+    breathe_stat = ["r0", "r1", "r2", "r3", "r4"]   # 呼吸状态:呼吸过慢/异常,深睡,浅睡,REM/清醒,呼吸过快/清醒
+    # 深睡(deep),浅睡(light),REM,清醒(awake),离床(leave)
+    sleep_stat = [
+        ["deep", "deep", "light", "REM", "awake"],      # peaceful
+        ["deep", "light", "light", "REM", "awake"],     # micro
+        ["awake", "awake", "awake", "awake", "awake"],  # active
+        ["leave", "leave", "leave", "leave", "leave"]]  # leave
+    # 异常:呼吸过慢(slow_breathe),呼吸过快(fast_breathe)
+
+    def __init__(self, event_type):
+        self.start_sleep_ts_: int   = -1    # 睡眠开始时间
+        self.end_sleep_ts_: int     = -1    # 睡眠结束时间
+        self.motion_stat_   = None  # 当前运动状态
+        self.breathe_stat_  = None  # 当前呼吸状态
+        self.sleep_stat_    = None  # 当前睡眠状态
+        self.sleep_segments_    = []    # 状态阶段记录 [{"ts":xxx, "stat":xxx}, ...]
+        self.last_motion_   = 0.0   # 最新的运动状态
+        self.last_breath_   = 0.0   # 最新的呼吸率
+        self.last_pts_      = []    # 最后的点目标
+        self.last_update_ts_    = -1
+        self.miss_target_count_ = 0
+
+        return
+
+
 # 事件属性 清理过期事件
 class EventAttr_CleanExpireEvents(EventAttr_Base):
     def __init__(self, event_type):
@@ -180,6 +210,8 @@ event_attr_map = {
     EventType.BATHROOM_STAY_FREQUENCY.value         : EventAttr_BathroomStayFrequency,
     EventType.TARGET_ABSENCE.value                  : EventAttr_TargetAbsence,
 
+    EventType.SLEEP_MONITORING.value                : EventAttr_SleepMonitoring,
+
     EventType.CLEAN_EXPIRE_EVENTS.value             : EventAttr_CleanExpireEvents,
 }
 
@@ -202,6 +234,10 @@ class AlarmPlan:
         EventType.NIGHT_TOILETING_FREQUENCY_ABNORMAL.value : lambda plan: AlarmPlan.handle_night_toileting_frequency_abnormal(plan),
         EventType.BATHROOM_STAY_FREQUENCY.value         : lambda plan: AlarmPlan.handle_bathroom_stay_frequency(plan),
         EventType.TARGET_ABSENCE.value                  : lambda plan: AlarmPlan.handle_target_absence(plan),
+
+        EventType.SLEEP_MONITORING.value                : lambda plan: AlarmPlan.handle_sleep_monitoring(plan),
+
+        # 平台事件(任务)
         EventType.CLEAN_EXPIRE_EVENTS.value             : lambda plan: AlarmPlan.handle_clear_expire_events(plan),
     }
 
@@ -232,7 +268,7 @@ class AlarmPlan:
         self.linkage_action_    = linkage_action    # 联动动作
         self.tenant_id_     = tenant_id     # 租户id
 
-        # 维护状态(根据TimePlanu判断)
+        # 维护状态(根据TimePlan判断)
         self.status_ = 0     # 0未激活,1激活,-1过期
         self.status_update_ts_ = -1   # 状态更新时间,初始值为-1
 
@@ -833,6 +869,172 @@ class AlarmPlan:
                 LOGERR(f"[{frame.filename}:{frame.lineno}] @{frame.name}(), error: {e}")
 
 
+    # 睡眠监测
+    def handle_sleep_monitoring(self):
+        # ---------------- 参数配置 ----------------
+        NO_TARGET_TIMEOUT_S = 3          # 超过3秒无数据认为无目标
+        NO_TARGET_MAX_COUNT = 3          # 连续3次无目标后确定离床
+        MOTION_SMOOTH_WINDOW = 10        # 平滑窗口大小(取最近10帧计算平均)
+        STAY_THRESHOLD_PEACEFUL = 0.05   # 静止阈值
+        STAY_THRESHOLD_MICRO = 0.15      # 微动阈值
+        LEAVE_BED_TS        = 3         # 离床判定时间阈值
+
+        try:
+            dev_id = self.dev_id_
+            device:Device = g_Dev.g_dev_mgr.find_dev_map(dev_id)
+            if not device:
+                return
+            if (not self.rect_):
+                return
+
+            rtd_list = device.get_rtd_que_copy()
+            if not rtd_list:
+                return
+
+            # 初始化状态机
+            if not hasattr(self, "sleep_stat_"):
+                self.sleep_stat_: str   = "leave"   # 当前睡眠状态
+                self.update_sleep_stat_: str   = "leave" # 要更新的睡眠状态
+                self.avg_motion_: float = 0.0       # 当前运动幅值
+                self.motion_stat_: str  = "leave"   # 当前运动状态
+                self.avg_breath_: float = 0.0       # 当前呼吸率
+                self.breathe_stat_: str = "r0"      # 当前呼吸状态
+
+                self.start_sleep_ts_: int   = -1    # 睡眠开始时间
+                self.end_sleep_ts_: int     = -1    # 睡眠结束时间
+
+                self.motion_window = deque(maxlen=MOTION_SMOOTH_WINDOW) # 近 N 次运动距离均值
+                self.sleep_segments_    = []    # 状态阶段记录 [{"ts":xxx, "stat":xxx}, ...]
+                self.last_pts_          = []        # 最后的点目标
+                self.last_update_ts_    = -1
+                self.miss_target_count_ = 0
+                self.last_leave_ts = -1 # 上次离床判定时间,离床判定时间超过5秒视为离床事件
+
+            now_ts  = get_utc_time_s()
+            rtd_unit = rtd_list[-1]
+            ts = rtd_unit["timestamp"]
+            target_point = rtd_unit["target_point"]
+
+            ## 1. 空间状态分析
+            if now_ts - ts > NO_TARGET_TIMEOUT_S:
+                ## 目标不存在
+                # 起夜
+                x, y, z, snr = target_point
+                if not helper.is_point_in_rect(x, y, self.rect_):
+                    motion = "leave"
+
+                # 检测不到体动
+                if self.start_sleep_ts_ == -1:
+                    return
+                else:
+                    motion = "peaceful"
+            else:
+                ## 目标存在
+                x = target_point[0]
+                y = target_point[1]
+                if not helper.is_point_in_rect(x, y, self.rect_):
+                    # 不在床上
+                    motion = "leave"
+                else:
+                    # 在床上
+                    # 计算体动
+                    WINDOWS_S = 10    # 滑动事件窗口
+                    recent_rtds = [r for r in rtd_list if now_ts - r["timestamp"] <= WINDOWS_S]
+                    if len(recent_rtds) < 5:
+                        return
+                    
+                    # 取最新点
+                    x,y,z,snr = recent_rtds[-1]["target_point"]
+                    # 检查是否在床上
+                    if not helper.is_point_in_rect(x, y, self.rect_):
+                        if self.last_leave_ts == -1:
+                            self.last_leave_ts = now_ts
+                        elif now_ts - self.last_leave_ts > LEAVE_BED_TS:
+                            motion = "leave"
+                    else:
+                        self.last_leave_ts = -1
+                        # 计算位移序列
+                        motions = []
+                        for i in range(1, len(recent_rtds)):
+                            x1, y1, z1, _ = recent_rtds[i - 1]["target_point"]
+                            x2, y2, z2, _ = recent_rtds[i]["target_point"]
+                            dist = ((x2 - x1)**2 + (y2 - y1)**2 + (z2 - z1)**2)**0.5
+                            motions.append(dist)
+                        if motions:
+                            avg_motion = sum(motions) / len(motions)
+                            self.motion_window.append(avg_motion)
+                            motion_smooth = sum(self.motion_window) / len(self.motion_window)
+                        else:
+                            motion_smooth = 0
+
+                    # 状态判定
+                    if motion_smooth < STAY_THRESHOLD_PEACEFUL:
+                        motion = "peaceful"
+                    elif motion_smooth < STAY_THRESHOLD_MICRO:
+                        motion = "mocro"
+                    else:
+                        motion = "active"
+
+                self.motion_stat_ = motion
+
+            ## 2. 呼吸率分析
+            BREATHE_WINDOWS_S = 5   # 呼吸滑动时间窗口
+            recent_breaths =[r["breath_rpm"] for r in rtd_list
+                             if (now_ts - r["timestamp"] <= BREATHE_WINDOWS_S) and
+                             ("breath_rpm" in r and isinstance(r["breath_rpm"], (int, float)))]
+            
+            if not recent_breaths:
+                breathe_stat = "r0" # 无数据时视为异常/无呼吸
+                avg_breath = 0.0
+            else:
+                avg_breath = sum(recent_breaths) / len(recent_breaths)
+
+                if avg_breath < 8:
+                    breathe_stat = "r0"
+                elif avg_breath < 12:
+                    breathe_stat = "r1"
+                elif avg_breath < 18:
+                    breathe_stat = "r1"
+                else:
+                    breathe_stat = "r1"
+
+            self.avg_breath_ = avg_breath
+            self.breathe_stat_ = breathe_stat
+
+            ## 3. 睡眠状态分析
+            try:
+                i_motion = EventAttr_SleepMonitoring.motion_stat.index(self.motion_stat_)
+                i_breath = EventAttr_SleepMonitoring.breathe_stat.index(breathe_stat)
+                self.update_sleep_stat_ = EventAttr_SleepMonitoring.sleep_stat[i_motion][i_breath]
+            except ValueError:
+                LOGERR(f"infalid i_montion or i_breath")
+
+
+            # 3.1 更新睡眠报告
+            sleep_node = {
+                "ts": now_ts,
+                "sleep_stat": self.update_sleep_stat_
+            }
+            self.sleep_segments_.append(sleep_node)
+
+            self.sleep_stat_ = self.update_sleep_stat_
+
+
+            # 1. 分析空间状态
+            # 2. 分析呼吸率
+            # 3. 分析睡眠状态
+            return
+
+        except json.JSONDecodeError as e:
+            tb_info = traceback.extract_tb(e.__traceback__)
+            for frame in tb_info:
+                LOGERR(f"[{frame.filename}:{frame.lineno}] @{frame.name}(), error:{e}, {e.doc}")
+        except Exception as e:
+            tb_info = traceback.extract_tb(e.__traceback__)
+            for frame in tb_info:
+                LOGERR(f"[{frame.filename}:{frame.lineno}] @{frame.name}(), error: {e}")
+
+
     # 清理过期事件
     def handle_clear_expire_events(self):
         try:

+ 12 - 0
core/alarm_plan_helper.py

@@ -51,5 +51,17 @@ def normalize_param_time(param: dict, now: datetime = None):
     }
 
 
+def is_point_in_rect(x, y, rect):
+    """
+    判断点 (x, y) 是否在 rect 定义的矩形内
+    rect 格式: [left, top, w, h]
+    """
+    if not rect or len(rect) != 4:
+        return False
+
+    left, top, w, h = rect
+    right = left + w
+    bottom = top + h
 
+    return left <= x <= right and top <= y <= bottom
 

+ 12 - 8
core/event_type.py

@@ -3,15 +3,17 @@ from enum import Enum
 # 事件类型
 class EventType(Enum):
     # 设备事件
-    STAY_DETECTION                  = 1 # 停留事件
-    RETENTION_DETECTION             = 2 # 滞留事件
-    TOILETING_DETECTION             = 3 # 如厕事件
-    TOILETING_FREQUENCY             = 4 # 如厕频次统计
-    NIGHT_TOILETING_FREQUENCY       = 5 # 夜间如厕频次统计
-    TOILETING_FREQUENCY_ABNORMAL    = 6 # 如厕频次异常
+    STAY_DETECTION                  = 1     # 停留事件
+    RETENTION_DETECTION             = 2     # 滞留事件
+    TOILETING_DETECTION             = 3     # 如厕事件
+    TOILETING_FREQUENCY             = 4     # 如厕频次统计
+    NIGHT_TOILETING_FREQUENCY       = 5     # 夜间如厕频次统计
+    TOILETING_FREQUENCY_ABNORMAL    = 6     # 如厕频次异常
     NIGHT_TOILETING_FREQUENCY_ABNORMAL  = 7 # 起夜异常
-    BATHROOM_STAY_FREQUENCY         = 8 # 卫生间频次统计
-    TARGET_ABSENCE                  = 9 # 异常消失
+    BATHROOM_STAY_FREQUENCY         = 8     # 卫生间频次统计
+    TARGET_ABSENCE                  = 9     # 异常消失
+
+    SLEEP_MONITORING                = 10    # 睡眠检测
 
     # 平台事件(任务)
     CLEAN_EXPIRE_EVENTS             = 9001  # 清理过期事件
@@ -28,6 +30,8 @@ event_desc_map = {
     EventType.BATHROOM_STAY_FREQUENCY.value     : "bathroom_stay_frequency",
     EventType.TARGET_ABSENCE.value              : "target_absence",
 
+    EventType.SLEEP_MONITORING.value            : "sleep_monitoring",
+
     # 平台事件(任务)
     EventType.CLEAN_EXPIRE_EVENTS.value         : "clear_expire_events"
 }

+ 11 - 1
device/dev_mng.py

@@ -97,7 +97,7 @@ class Device():
         self.keepalive_: int    = get_utc_time_s()
 
         # 实时数据队列
-        self.rtd_len_: int = 100
+        self.rtd_len_: int = 600
         self.rtd_que_: deque = deque(maxlen=self.rtd_len_)
         """
 {
@@ -244,12 +244,22 @@ class DeviceManager():
             if result:
                 for row in result:
                     dev_id = row["client_id"]
+                    x1 = row["start_x"]
+                    y1 = row["start_y"]
+                    z1 = row["start_z"]
+                    x2 = row["stop_x"]
+                    y2 = row["stop_y"]
+                    z2 = row["stop_z"]
+                    mount_plain = row["mount_plain"]
+                    height = row["height"]
+                    install_param = InstallParam(mount_plain, round(height,3), TrackingRegion(x1, y1, z1, x2, y2, z2))
                     dev_instance = Device(
                         dev_id=row["client_id"],
                         dev_name=row["dev_name"],
                         online=row["online"],
                         dev_type=row["dev_type"]
                     )
+                    dev_instance.install_param_ = install_param
                     # 更新设备信息
                     self.push_dev_map(dev_id, dev_instance)
     

+ 10 - 5
mqtt/mqtt_recv.py

@@ -118,15 +118,20 @@ def deal_tracker_targets(msg:mqtt.MQTTMessage):
 
         # 处理 target
         if ("tracker_targets" in payload):
-            tracker_targets = payload["tracker_targets"]
-
             timestamp = get_utc_time_s()
+            tracker_targets = payload["tracker_targets"]
+            breath_rpm: float = 0.000
+            if (("breath_rpm" in payload) and 
+                ("breath_rpm" in payload["health"])):
+                breath_rpm = payload["health"]["breath_rpm"]
             pose = POSE_E.POSE_4.value
             rtd_unit = {
-                "timestamp": timestamp,
-                "pose": pose,
-                "target_point": tracker_targets
+                "timestamp"     : timestamp,
+                "target_point"  : tracker_targets,
+                "breath_rpm"    : breath_rpm,
+                "pose"          : pose
             }
+
             device.put_rtd_unit(rtd_unit)
             device.update_keepalive(timestamp)