diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/tcp/gas_device_handler.py b/tcp/gas_device_handler.py new file mode 100644 index 0000000..b9c5f6e --- /dev/null +++ b/tcp/gas_device_handler.py @@ -0,0 +1,111 @@ +import copy +import json +import asyncio +from datetime import datetime + +from common.byte_utils import format_bytes +from common.global_logger import logger +from common.http_utils import send_request_async +from db.database import get_db +from entity.data_gas import DataGas +from scene_handler.alarm_record_center import AlarmRecordCenter +from services.data_gas_service import DataGasService +from services.global_config import GlobalConfig + + +def parse_gas_data(data): + # 数据长度检查,确保最小长度符合协议要求 + if len(data) < 13: + return None + # raise ValueError("数据长度不足,无法解析") + + # 检查帧头(AA 01) + if data[6:8] != b'\xAA\x01': + return None + # raise ValueError("帧头不匹配") + + + # 解析设备编号(UU VV WW XX YY ZZ) + device_id = ''.join(f'{byte:02X}' for byte in data[:6]) + + + + # 解析 GG, HH, II 字节,计算燃气浓度值 (ppm.m) + GG = data[8] + HH = data[9] + II = data[10] + gas_concentration = GG * 65536 + HH * 256 + II + if gas_concentration > 99999: + raise ValueError("燃气浓度值超出范围") + + # 解析激光光强等级 JJ + JJ = data[11] + if not (0 <= JJ <= 14): + raise ValueError("激光光强等级超出范围") + + # 校验和 SU 验证,从第8到第12字节累加 + SU_received = data[12] + SU_calculated = sum(data[7:12]) & 0xFF # 取累加值的低8位 + if SU_received != SU_calculated: + raise ValueError(f"校验和不匹配: 预期 {SU_calculated:02X}, 实际 {SU_received:02X}") + + # 返回解析的结果 + return { + "device_code": device_id, + "gas_value": int(gas_concentration), + "laser_intensity_level": JJ, + "checksum_valid": SU_received == SU_calculated + } + + +# 业务处理层:解析气体数据、保存数据库、推送数据 +class GasDataHandler: + + alarm_dict = { + 'alarmType': '1', + 'alarmContent': '甲烷浓度超限', + } + + def __init__(self, main_loop=None): + self.last_push = {} + self.alarm_record_center = AlarmRecordCenter(main_loop=main_loop) + + async def handle_data(self, data: bytes): + try: + res = parse_gas_data(data) + if res: + logger.info(f"解析甲烷数据:{format_bytes(data)} {res}") + async for db in get_db(): + data_gas_service = DataGasService(db) + data_gas = DataGas( + device_code=res['device_code'], + gas_value=res['gas_value'] + ) + await data_gas_service.add_data_gas(data_gas) + self.push_data(data_gas) + self.handle_alarm(data_gas) + except Exception as e: + logger.exception(f"处理甲烷数据出错: {e}") + + def push_data(self, data_gas): + global_config = GlobalConfig() + gas_push_config = global_config.get_gas_push_config() + if gas_push_config and gas_push_config.push_url and gas_push_config.push_interval > 0: + last_ts = self.last_push.get(data_gas.device_code) + current_time = datetime.now() + + # 检查是否需要推送数据 + if last_ts is None or (current_time - last_ts).total_seconds() > gas_push_config.push_interval: + send_data = json.loads(copy.deepcopy(data_gas.json())) + send_data.pop("id") + asyncio.create_task(send_request_async(gas_push_config.push_url, send_data)) + self.last_push[data_gas.device_code] = current_time # 更新推送时间戳 + + def handle_alarm(self, data_gas): + if data_gas.gas_value > 100: + logger.info(f"甲烷浓度超限报警: {data_gas}") + self.alarm_record_center.upload_alarm_record(device_code=data_gas.device_code, + alarm_dict=self.alarm_dict, + alarm_value=data_gas.gas_value) + else: + logger.info(f"甲烷浓度正常: {data_gas}") \ No newline at end of file diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/tcp/gas_device_handler.py b/tcp/gas_device_handler.py new file mode 100644 index 0000000..b9c5f6e --- /dev/null +++ b/tcp/gas_device_handler.py @@ -0,0 +1,111 @@ +import copy +import json +import asyncio +from datetime import datetime + +from common.byte_utils import format_bytes +from common.global_logger import logger +from common.http_utils import send_request_async +from db.database import get_db +from entity.data_gas import DataGas +from scene_handler.alarm_record_center import AlarmRecordCenter +from services.data_gas_service import DataGasService +from services.global_config import GlobalConfig + + +def parse_gas_data(data): + # 数据长度检查,确保最小长度符合协议要求 + if len(data) < 13: + return None + # raise ValueError("数据长度不足,无法解析") + + # 检查帧头(AA 01) + if data[6:8] != b'\xAA\x01': + return None + # raise ValueError("帧头不匹配") + + + # 解析设备编号(UU VV WW XX YY ZZ) + device_id = ''.join(f'{byte:02X}' for byte in data[:6]) + + + + # 解析 GG, HH, II 字节,计算燃气浓度值 (ppm.m) + GG = data[8] + HH = data[9] + II = data[10] + gas_concentration = GG * 65536 + HH * 256 + II + if gas_concentration > 99999: + raise ValueError("燃气浓度值超出范围") + + # 解析激光光强等级 JJ + JJ = data[11] + if not (0 <= JJ <= 14): + raise ValueError("激光光强等级超出范围") + + # 校验和 SU 验证,从第8到第12字节累加 + SU_received = data[12] + SU_calculated = sum(data[7:12]) & 0xFF # 取累加值的低8位 + if SU_received != SU_calculated: + raise ValueError(f"校验和不匹配: 预期 {SU_calculated:02X}, 实际 {SU_received:02X}") + + # 返回解析的结果 + return { + "device_code": device_id, + "gas_value": int(gas_concentration), + "laser_intensity_level": JJ, + "checksum_valid": SU_received == SU_calculated + } + + +# 业务处理层:解析气体数据、保存数据库、推送数据 +class GasDataHandler: + + alarm_dict = { + 'alarmType': '1', + 'alarmContent': '甲烷浓度超限', + } + + def __init__(self, main_loop=None): + self.last_push = {} + self.alarm_record_center = AlarmRecordCenter(main_loop=main_loop) + + async def handle_data(self, data: bytes): + try: + res = parse_gas_data(data) + if res: + logger.info(f"解析甲烷数据:{format_bytes(data)} {res}") + async for db in get_db(): + data_gas_service = DataGasService(db) + data_gas = DataGas( + device_code=res['device_code'], + gas_value=res['gas_value'] + ) + await data_gas_service.add_data_gas(data_gas) + self.push_data(data_gas) + self.handle_alarm(data_gas) + except Exception as e: + logger.exception(f"处理甲烷数据出错: {e}") + + def push_data(self, data_gas): + global_config = GlobalConfig() + gas_push_config = global_config.get_gas_push_config() + if gas_push_config and gas_push_config.push_url and gas_push_config.push_interval > 0: + last_ts = self.last_push.get(data_gas.device_code) + current_time = datetime.now() + + # 检查是否需要推送数据 + if last_ts is None or (current_time - last_ts).total_seconds() > gas_push_config.push_interval: + send_data = json.loads(copy.deepcopy(data_gas.json())) + send_data.pop("id") + asyncio.create_task(send_request_async(gas_push_config.push_url, send_data)) + self.last_push[data_gas.device_code] = current_time # 更新推送时间戳 + + def handle_alarm(self, data_gas): + if data_gas.gas_value > 100: + logger.info(f"甲烷浓度超限报警: {data_gas}") + self.alarm_record_center.upload_alarm_record(device_code=data_gas.device_code, + alarm_dict=self.alarm_dict, + alarm_value=data_gas.gas_value) + else: + logger.info(f"甲烷浓度正常: {data_gas}") \ No newline at end of file diff --git a/tcp/harmful_device_handler.py b/tcp/harmful_device_handler.py index ecbdd9c..dd48070 100644 --- a/tcp/harmful_device_handler.py +++ b/tcp/harmful_device_handler.py @@ -101,10 +101,10 @@ # 判断异常并上报 if gas_type is not None and gas_data is not None: if self._is_data_alarm(device_code, gas_type, gas_data): - print(f"报警: {device_code}, {gas_type}, {gas_data}") + print(f"四合一浓度报警: {device_code}, {gas_type}, {gas_data}") self._save_and_send_alarm(device_code, gas_type, gas_data) - print(self._harmful_gas_manager.get_device_all_data(device_code)) + print(f'更新四合一{device_code}数据 {self._harmful_gas_manager.get_device_all_data(device_code)}') except json.JSONDecodeError: logger.error(f"JSON解析错误: {message}") @@ -197,10 +197,7 @@ encoded_bytes = base64.b64encode(message.encode('utf-8')) # 将字节编码结果转换为字符串 encoded_string = encoded_bytes.decode('utf-8') - logger.debug(f'before encode: {message}') - logger.debug(f'after encode: {encoded_string}') push_message = {"content": encoded_string} - logger.debug(f'body: {push_message}') asyncio.create_task(send_request_async(harmful_push_config.push_url, push_message)) self._push_ts_dict[device_code] = current_time # 更新推送时间戳 else: diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/tcp/gas_device_handler.py b/tcp/gas_device_handler.py new file mode 100644 index 0000000..b9c5f6e --- /dev/null +++ b/tcp/gas_device_handler.py @@ -0,0 +1,111 @@ +import copy +import json +import asyncio +from datetime import datetime + +from common.byte_utils import format_bytes +from common.global_logger import logger +from common.http_utils import send_request_async +from db.database import get_db +from entity.data_gas import DataGas +from scene_handler.alarm_record_center import AlarmRecordCenter +from services.data_gas_service import DataGasService +from services.global_config import GlobalConfig + + +def parse_gas_data(data): + # 数据长度检查,确保最小长度符合协议要求 + if len(data) < 13: + return None + # raise ValueError("数据长度不足,无法解析") + + # 检查帧头(AA 01) + if data[6:8] != b'\xAA\x01': + return None + # raise ValueError("帧头不匹配") + + + # 解析设备编号(UU VV WW XX YY ZZ) + device_id = ''.join(f'{byte:02X}' for byte in data[:6]) + + + + # 解析 GG, HH, II 字节,计算燃气浓度值 (ppm.m) + GG = data[8] + HH = data[9] + II = data[10] + gas_concentration = GG * 65536 + HH * 256 + II + if gas_concentration > 99999: + raise ValueError("燃气浓度值超出范围") + + # 解析激光光强等级 JJ + JJ = data[11] + if not (0 <= JJ <= 14): + raise ValueError("激光光强等级超出范围") + + # 校验和 SU 验证,从第8到第12字节累加 + SU_received = data[12] + SU_calculated = sum(data[7:12]) & 0xFF # 取累加值的低8位 + if SU_received != SU_calculated: + raise ValueError(f"校验和不匹配: 预期 {SU_calculated:02X}, 实际 {SU_received:02X}") + + # 返回解析的结果 + return { + "device_code": device_id, + "gas_value": int(gas_concentration), + "laser_intensity_level": JJ, + "checksum_valid": SU_received == SU_calculated + } + + +# 业务处理层:解析气体数据、保存数据库、推送数据 +class GasDataHandler: + + alarm_dict = { + 'alarmType': '1', + 'alarmContent': '甲烷浓度超限', + } + + def __init__(self, main_loop=None): + self.last_push = {} + self.alarm_record_center = AlarmRecordCenter(main_loop=main_loop) + + async def handle_data(self, data: bytes): + try: + res = parse_gas_data(data) + if res: + logger.info(f"解析甲烷数据:{format_bytes(data)} {res}") + async for db in get_db(): + data_gas_service = DataGasService(db) + data_gas = DataGas( + device_code=res['device_code'], + gas_value=res['gas_value'] + ) + await data_gas_service.add_data_gas(data_gas) + self.push_data(data_gas) + self.handle_alarm(data_gas) + except Exception as e: + logger.exception(f"处理甲烷数据出错: {e}") + + def push_data(self, data_gas): + global_config = GlobalConfig() + gas_push_config = global_config.get_gas_push_config() + if gas_push_config and gas_push_config.push_url and gas_push_config.push_interval > 0: + last_ts = self.last_push.get(data_gas.device_code) + current_time = datetime.now() + + # 检查是否需要推送数据 + if last_ts is None or (current_time - last_ts).total_seconds() > gas_push_config.push_interval: + send_data = json.loads(copy.deepcopy(data_gas.json())) + send_data.pop("id") + asyncio.create_task(send_request_async(gas_push_config.push_url, send_data)) + self.last_push[data_gas.device_code] = current_time # 更新推送时间戳 + + def handle_alarm(self, data_gas): + if data_gas.gas_value > 100: + logger.info(f"甲烷浓度超限报警: {data_gas}") + self.alarm_record_center.upload_alarm_record(device_code=data_gas.device_code, + alarm_dict=self.alarm_dict, + alarm_value=data_gas.gas_value) + else: + logger.info(f"甲烷浓度正常: {data_gas}") \ No newline at end of file diff --git a/tcp/harmful_device_handler.py b/tcp/harmful_device_handler.py index ecbdd9c..dd48070 100644 --- a/tcp/harmful_device_handler.py +++ b/tcp/harmful_device_handler.py @@ -101,10 +101,10 @@ # 判断异常并上报 if gas_type is not None and gas_data is not None: if self._is_data_alarm(device_code, gas_type, gas_data): - print(f"报警: {device_code}, {gas_type}, {gas_data}") + print(f"四合一浓度报警: {device_code}, {gas_type}, {gas_data}") self._save_and_send_alarm(device_code, gas_type, gas_data) - print(self._harmful_gas_manager.get_device_all_data(device_code)) + print(f'更新四合一{device_code}数据 {self._harmful_gas_manager.get_device_all_data(device_code)}') except json.JSONDecodeError: logger.error(f"JSON解析错误: {message}") @@ -197,10 +197,7 @@ encoded_bytes = base64.b64encode(message.encode('utf-8')) # 将字节编码结果转换为字符串 encoded_string = encoded_bytes.decode('utf-8') - logger.debug(f'before encode: {message}') - logger.debug(f'after encode: {encoded_string}') push_message = {"content": encoded_string} - logger.debug(f'body: {push_message}') asyncio.create_task(send_request_async(harmful_push_config.push_url, push_message)) self._push_ts_dict[device_code] = current_time # 更新推送时间戳 else: diff --git a/tcp/tcp_client_connector.py b/tcp/tcp_client_connector.py index 6e0b776..1fb81e9 100644 --- a/tcp/tcp_client_connector.py +++ b/tcp/tcp_client_connector.py @@ -13,7 +13,9 @@ from services.data_gas_service import DataGasService from services.global_config import GlobalConfig - +''' +这个文件目前没有用了 +''' def parse_gas_data(data): # 数据长度检查,确保最小长度符合协议要求 if len(data) < 13: diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/tcp/gas_device_handler.py b/tcp/gas_device_handler.py new file mode 100644 index 0000000..b9c5f6e --- /dev/null +++ b/tcp/gas_device_handler.py @@ -0,0 +1,111 @@ +import copy +import json +import asyncio +from datetime import datetime + +from common.byte_utils import format_bytes +from common.global_logger import logger +from common.http_utils import send_request_async +from db.database import get_db +from entity.data_gas import DataGas +from scene_handler.alarm_record_center import AlarmRecordCenter +from services.data_gas_service import DataGasService +from services.global_config import GlobalConfig + + +def parse_gas_data(data): + # 数据长度检查,确保最小长度符合协议要求 + if len(data) < 13: + return None + # raise ValueError("数据长度不足,无法解析") + + # 检查帧头(AA 01) + if data[6:8] != b'\xAA\x01': + return None + # raise ValueError("帧头不匹配") + + + # 解析设备编号(UU VV WW XX YY ZZ) + device_id = ''.join(f'{byte:02X}' for byte in data[:6]) + + + + # 解析 GG, HH, II 字节,计算燃气浓度值 (ppm.m) + GG = data[8] + HH = data[9] + II = data[10] + gas_concentration = GG * 65536 + HH * 256 + II + if gas_concentration > 99999: + raise ValueError("燃气浓度值超出范围") + + # 解析激光光强等级 JJ + JJ = data[11] + if not (0 <= JJ <= 14): + raise ValueError("激光光强等级超出范围") + + # 校验和 SU 验证,从第8到第12字节累加 + SU_received = data[12] + SU_calculated = sum(data[7:12]) & 0xFF # 取累加值的低8位 + if SU_received != SU_calculated: + raise ValueError(f"校验和不匹配: 预期 {SU_calculated:02X}, 实际 {SU_received:02X}") + + # 返回解析的结果 + return { + "device_code": device_id, + "gas_value": int(gas_concentration), + "laser_intensity_level": JJ, + "checksum_valid": SU_received == SU_calculated + } + + +# 业务处理层:解析气体数据、保存数据库、推送数据 +class GasDataHandler: + + alarm_dict = { + 'alarmType': '1', + 'alarmContent': '甲烷浓度超限', + } + + def __init__(self, main_loop=None): + self.last_push = {} + self.alarm_record_center = AlarmRecordCenter(main_loop=main_loop) + + async def handle_data(self, data: bytes): + try: + res = parse_gas_data(data) + if res: + logger.info(f"解析甲烷数据:{format_bytes(data)} {res}") + async for db in get_db(): + data_gas_service = DataGasService(db) + data_gas = DataGas( + device_code=res['device_code'], + gas_value=res['gas_value'] + ) + await data_gas_service.add_data_gas(data_gas) + self.push_data(data_gas) + self.handle_alarm(data_gas) + except Exception as e: + logger.exception(f"处理甲烷数据出错: {e}") + + def push_data(self, data_gas): + global_config = GlobalConfig() + gas_push_config = global_config.get_gas_push_config() + if gas_push_config and gas_push_config.push_url and gas_push_config.push_interval > 0: + last_ts = self.last_push.get(data_gas.device_code) + current_time = datetime.now() + + # 检查是否需要推送数据 + if last_ts is None or (current_time - last_ts).total_seconds() > gas_push_config.push_interval: + send_data = json.loads(copy.deepcopy(data_gas.json())) + send_data.pop("id") + asyncio.create_task(send_request_async(gas_push_config.push_url, send_data)) + self.last_push[data_gas.device_code] = current_time # 更新推送时间戳 + + def handle_alarm(self, data_gas): + if data_gas.gas_value > 100: + logger.info(f"甲烷浓度超限报警: {data_gas}") + self.alarm_record_center.upload_alarm_record(device_code=data_gas.device_code, + alarm_dict=self.alarm_dict, + alarm_value=data_gas.gas_value) + else: + logger.info(f"甲烷浓度正常: {data_gas}") \ No newline at end of file diff --git a/tcp/harmful_device_handler.py b/tcp/harmful_device_handler.py index ecbdd9c..dd48070 100644 --- a/tcp/harmful_device_handler.py +++ b/tcp/harmful_device_handler.py @@ -101,10 +101,10 @@ # 判断异常并上报 if gas_type is not None and gas_data is not None: if self._is_data_alarm(device_code, gas_type, gas_data): - print(f"报警: {device_code}, {gas_type}, {gas_data}") + print(f"四合一浓度报警: {device_code}, {gas_type}, {gas_data}") self._save_and_send_alarm(device_code, gas_type, gas_data) - print(self._harmful_gas_manager.get_device_all_data(device_code)) + print(f'更新四合一{device_code}数据 {self._harmful_gas_manager.get_device_all_data(device_code)}') except json.JSONDecodeError: logger.error(f"JSON解析错误: {message}") @@ -197,10 +197,7 @@ encoded_bytes = base64.b64encode(message.encode('utf-8')) # 将字节编码结果转换为字符串 encoded_string = encoded_bytes.decode('utf-8') - logger.debug(f'before encode: {message}') - logger.debug(f'after encode: {encoded_string}') push_message = {"content": encoded_string} - logger.debug(f'body: {push_message}') asyncio.create_task(send_request_async(harmful_push_config.push_url, push_message)) self._push_ts_dict[device_code] = current_time # 更新推送时间戳 else: diff --git a/tcp/tcp_client_connector.py b/tcp/tcp_client_connector.py index 6e0b776..1fb81e9 100644 --- a/tcp/tcp_client_connector.py +++ b/tcp/tcp_client_connector.py @@ -13,7 +13,9 @@ from services.data_gas_service import DataGasService from services.global_config import GlobalConfig - +''' +这个文件目前没有用了 +''' def parse_gas_data(data): # 数据长度检查,确保最小长度符合协议要求 if len(data) < 13: diff --git a/tcp/tcp_client_manager.py b/tcp/tcp_client_manager.py new file mode 100644 index 0000000..74d9c8d --- /dev/null +++ b/tcp/tcp_client_manager.py @@ -0,0 +1,48 @@ +import asyncio +from typing import List, Dict + +from common.consts import DEVICE_TYPE, NotifyChangeType, TREE_COMMAND +from db.database import get_db +from entity.device import Device +from services.device_service import DeviceService + +from common.global_logger import logger +from tcp.gas_device_handler import GasDataHandler +from tcp.tcp_connection import TcpConnection + + +# TcpClientManager 负责管理各设备的 TcpConnection,并提供对外接口 +class TcpClientManager: + def __init__(self, device_service, main_loop=None): + self.main_loop = main_loop + self.device_service = device_service + self.connector_map = {} # device_id -> TcpConnection + + async def start(self): + """从数据库加载设备并连接所有设备""" + devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 + logger.info(f"get {len(devices)} tree devices") + for device in devices: + await self.start_device_connect(device) + + async def start_device_connect(self, device): + if device.id in self.connector_map: + logger.warning(f"设备 {device.id} 已连接") + return + connector = TcpConnection(ip=device.gas_ip, port=333) + asyncio.create_task(connector.connection_monitor()) + handler = GasDataHandler(main_loop=self.main_loop) + connector.register_data_handler(handler.handle_data) + self.connector_map[device.id] = connector + asyncio.create_task(connector.connect()) + # 启动定时查询任务(查询间隔由设备配置决定) + asyncio.create_task( + connector.start_periodic_query(query_command=TREE_COMMAND.GAS_QUERY, interval=3)) + + async def send_message_to_device(self, device_id: int, message: bytes, have_response: bool = True): + if device_id not in self.connector_map: + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + connector = self.connector_map.get(device_id) + if connector: + await connector.send_message(message, have_response=have_response) diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/tcp/gas_device_handler.py b/tcp/gas_device_handler.py new file mode 100644 index 0000000..b9c5f6e --- /dev/null +++ b/tcp/gas_device_handler.py @@ -0,0 +1,111 @@ +import copy +import json +import asyncio +from datetime import datetime + +from common.byte_utils import format_bytes +from common.global_logger import logger +from common.http_utils import send_request_async +from db.database import get_db +from entity.data_gas import DataGas +from scene_handler.alarm_record_center import AlarmRecordCenter +from services.data_gas_service import DataGasService +from services.global_config import GlobalConfig + + +def parse_gas_data(data): + # 数据长度检查,确保最小长度符合协议要求 + if len(data) < 13: + return None + # raise ValueError("数据长度不足,无法解析") + + # 检查帧头(AA 01) + if data[6:8] != b'\xAA\x01': + return None + # raise ValueError("帧头不匹配") + + + # 解析设备编号(UU VV WW XX YY ZZ) + device_id = ''.join(f'{byte:02X}' for byte in data[:6]) + + + + # 解析 GG, HH, II 字节,计算燃气浓度值 (ppm.m) + GG = data[8] + HH = data[9] + II = data[10] + gas_concentration = GG * 65536 + HH * 256 + II + if gas_concentration > 99999: + raise ValueError("燃气浓度值超出范围") + + # 解析激光光强等级 JJ + JJ = data[11] + if not (0 <= JJ <= 14): + raise ValueError("激光光强等级超出范围") + + # 校验和 SU 验证,从第8到第12字节累加 + SU_received = data[12] + SU_calculated = sum(data[7:12]) & 0xFF # 取累加值的低8位 + if SU_received != SU_calculated: + raise ValueError(f"校验和不匹配: 预期 {SU_calculated:02X}, 实际 {SU_received:02X}") + + # 返回解析的结果 + return { + "device_code": device_id, + "gas_value": int(gas_concentration), + "laser_intensity_level": JJ, + "checksum_valid": SU_received == SU_calculated + } + + +# 业务处理层:解析气体数据、保存数据库、推送数据 +class GasDataHandler: + + alarm_dict = { + 'alarmType': '1', + 'alarmContent': '甲烷浓度超限', + } + + def __init__(self, main_loop=None): + self.last_push = {} + self.alarm_record_center = AlarmRecordCenter(main_loop=main_loop) + + async def handle_data(self, data: bytes): + try: + res = parse_gas_data(data) + if res: + logger.info(f"解析甲烷数据:{format_bytes(data)} {res}") + async for db in get_db(): + data_gas_service = DataGasService(db) + data_gas = DataGas( + device_code=res['device_code'], + gas_value=res['gas_value'] + ) + await data_gas_service.add_data_gas(data_gas) + self.push_data(data_gas) + self.handle_alarm(data_gas) + except Exception as e: + logger.exception(f"处理甲烷数据出错: {e}") + + def push_data(self, data_gas): + global_config = GlobalConfig() + gas_push_config = global_config.get_gas_push_config() + if gas_push_config and gas_push_config.push_url and gas_push_config.push_interval > 0: + last_ts = self.last_push.get(data_gas.device_code) + current_time = datetime.now() + + # 检查是否需要推送数据 + if last_ts is None or (current_time - last_ts).total_seconds() > gas_push_config.push_interval: + send_data = json.loads(copy.deepcopy(data_gas.json())) + send_data.pop("id") + asyncio.create_task(send_request_async(gas_push_config.push_url, send_data)) + self.last_push[data_gas.device_code] = current_time # 更新推送时间戳 + + def handle_alarm(self, data_gas): + if data_gas.gas_value > 100: + logger.info(f"甲烷浓度超限报警: {data_gas}") + self.alarm_record_center.upload_alarm_record(device_code=data_gas.device_code, + alarm_dict=self.alarm_dict, + alarm_value=data_gas.gas_value) + else: + logger.info(f"甲烷浓度正常: {data_gas}") \ No newline at end of file diff --git a/tcp/harmful_device_handler.py b/tcp/harmful_device_handler.py index ecbdd9c..dd48070 100644 --- a/tcp/harmful_device_handler.py +++ b/tcp/harmful_device_handler.py @@ -101,10 +101,10 @@ # 判断异常并上报 if gas_type is not None and gas_data is not None: if self._is_data_alarm(device_code, gas_type, gas_data): - print(f"报警: {device_code}, {gas_type}, {gas_data}") + print(f"四合一浓度报警: {device_code}, {gas_type}, {gas_data}") self._save_and_send_alarm(device_code, gas_type, gas_data) - print(self._harmful_gas_manager.get_device_all_data(device_code)) + print(f'更新四合一{device_code}数据 {self._harmful_gas_manager.get_device_all_data(device_code)}') except json.JSONDecodeError: logger.error(f"JSON解析错误: {message}") @@ -197,10 +197,7 @@ encoded_bytes = base64.b64encode(message.encode('utf-8')) # 将字节编码结果转换为字符串 encoded_string = encoded_bytes.decode('utf-8') - logger.debug(f'before encode: {message}') - logger.debug(f'after encode: {encoded_string}') push_message = {"content": encoded_string} - logger.debug(f'body: {push_message}') asyncio.create_task(send_request_async(harmful_push_config.push_url, push_message)) self._push_ts_dict[device_code] = current_time # 更新推送时间戳 else: diff --git a/tcp/tcp_client_connector.py b/tcp/tcp_client_connector.py index 6e0b776..1fb81e9 100644 --- a/tcp/tcp_client_connector.py +++ b/tcp/tcp_client_connector.py @@ -13,7 +13,9 @@ from services.data_gas_service import DataGasService from services.global_config import GlobalConfig - +''' +这个文件目前没有用了 +''' def parse_gas_data(data): # 数据长度检查,确保最小长度符合协议要求 if len(data) < 13: diff --git a/tcp/tcp_client_manager.py b/tcp/tcp_client_manager.py new file mode 100644 index 0000000..74d9c8d --- /dev/null +++ b/tcp/tcp_client_manager.py @@ -0,0 +1,48 @@ +import asyncio +from typing import List, Dict + +from common.consts import DEVICE_TYPE, NotifyChangeType, TREE_COMMAND +from db.database import get_db +from entity.device import Device +from services.device_service import DeviceService + +from common.global_logger import logger +from tcp.gas_device_handler import GasDataHandler +from tcp.tcp_connection import TcpConnection + + +# TcpClientManager 负责管理各设备的 TcpConnection,并提供对外接口 +class TcpClientManager: + def __init__(self, device_service, main_loop=None): + self.main_loop = main_loop + self.device_service = device_service + self.connector_map = {} # device_id -> TcpConnection + + async def start(self): + """从数据库加载设备并连接所有设备""" + devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 + logger.info(f"get {len(devices)} tree devices") + for device in devices: + await self.start_device_connect(device) + + async def start_device_connect(self, device): + if device.id in self.connector_map: + logger.warning(f"设备 {device.id} 已连接") + return + connector = TcpConnection(ip=device.gas_ip, port=333) + asyncio.create_task(connector.connection_monitor()) + handler = GasDataHandler(main_loop=self.main_loop) + connector.register_data_handler(handler.handle_data) + self.connector_map[device.id] = connector + asyncio.create_task(connector.connect()) + # 启动定时查询任务(查询间隔由设备配置决定) + asyncio.create_task( + connector.start_periodic_query(query_command=TREE_COMMAND.GAS_QUERY, interval=3)) + + async def send_message_to_device(self, device_id: int, message: bytes, have_response: bool = True): + if device_id not in self.connector_map: + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + connector = self.connector_map.get(device_id) + if connector: + await connector.send_message(message, have_response=have_response) diff --git a/tcp/tcp_client_manager.py.bak b/tcp/tcp_client_manager.py.bak new file mode 100644 index 0000000..22a0d89 --- /dev/null +++ b/tcp/tcp_client_manager.py.bak @@ -0,0 +1,89 @@ +import asyncio +from typing import List, Dict + + +from common.consts import DEVICE_TYPE, NotifyChangeType +from db.database import get_db +from entity.device import Device +from services.device_service import DeviceService + +from common.global_logger import logger +from services.global_config import GlobalConfig +from tcp.tcp_client_connector import TcpClientConnector + + +class TcpClientManager: + def __init__(self, device_service: DeviceService): + self.devices: List[Device] = [] + self.connector_map: Dict[int, TcpClientConnector] = {} + + self.device_service = device_service + + # 注册设备和模型的变化回调 + # self.device_service.register_change_callback(self.on_device_change) + + async def load_and_connect_devices(self): + """从数据库加载设备并连接所有设备""" + devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 + logger.info(f"get {len(devices)} tree devices") + for device in devices: + await self.start_device_connect(device) + + async def start(self): + """启动设备管理器""" + await self.load_and_connect_devices() + + async def start_device_connect(self, device: Device): + if device and int(device.type) == DEVICE_TYPE.TREE and device.gas_ip: + if device.id in self.connector_map: + logger.warning(f"Device {device.id} is already connected.") + return # 防止重复连接 + connector = TcpClientConnector(ip=device.gas_ip, port=333) + self.connector_map[device.id] = connector + asyncio.create_task(connector.connect()) + + async def stop_device_connect(self, device_id): + if device_id in self.connector_map: + connector = self.connector_map.pop(device_id) + await self.disconnect_device(connector) + + async def restart_device_thread(self, device_id): + await self.stop_device_connect(device_id) + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + + async def on_device_change(self, device_id, change_type): + """设备变化时的回调处理""" + if change_type == NotifyChangeType.DEVICE_CREATE: + # 新增设备,加载新设备并连接 + new_device = await self.device_service.get_device(device_id) + await self.start_device_connect(new_device) + + elif change_type == NotifyChangeType.DEVICE_DELETE: + await self.stop_device_connect(device_id) + + elif change_type == NotifyChangeType.DEVICE_UPDATE: + # 更新设备信息,重新连接 + await self.restart_device_thread(device_id) + + async def disconnect_device(self, connector: TcpClientConnector): + """断开设备连接""" + await connector.disconnect() + + async def send_message_to_device(self, device_id, message: bytes, have_response): + if device_id not in self.connector_map: + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + connector = self.connector_map[device_id] + if connector: + await connector.send_message(message, have_response=have_response) + + + +# if __name__ == '__main__': + # async for db in get_db(): + # global_config = GlobalConfig() + # await global_config.init_config() + # device_service = DeviceService(db) + # tcp_manager = TcpManager(device_service) + # asyncio.run(tcp_manager.start()) diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/tcp/gas_device_handler.py b/tcp/gas_device_handler.py new file mode 100644 index 0000000..b9c5f6e --- /dev/null +++ b/tcp/gas_device_handler.py @@ -0,0 +1,111 @@ +import copy +import json +import asyncio +from datetime import datetime + +from common.byte_utils import format_bytes +from common.global_logger import logger +from common.http_utils import send_request_async +from db.database import get_db +from entity.data_gas import DataGas +from scene_handler.alarm_record_center import AlarmRecordCenter +from services.data_gas_service import DataGasService +from services.global_config import GlobalConfig + + +def parse_gas_data(data): + # 数据长度检查,确保最小长度符合协议要求 + if len(data) < 13: + return None + # raise ValueError("数据长度不足,无法解析") + + # 检查帧头(AA 01) + if data[6:8] != b'\xAA\x01': + return None + # raise ValueError("帧头不匹配") + + + # 解析设备编号(UU VV WW XX YY ZZ) + device_id = ''.join(f'{byte:02X}' for byte in data[:6]) + + + + # 解析 GG, HH, II 字节,计算燃气浓度值 (ppm.m) + GG = data[8] + HH = data[9] + II = data[10] + gas_concentration = GG * 65536 + HH * 256 + II + if gas_concentration > 99999: + raise ValueError("燃气浓度值超出范围") + + # 解析激光光强等级 JJ + JJ = data[11] + if not (0 <= JJ <= 14): + raise ValueError("激光光强等级超出范围") + + # 校验和 SU 验证,从第8到第12字节累加 + SU_received = data[12] + SU_calculated = sum(data[7:12]) & 0xFF # 取累加值的低8位 + if SU_received != SU_calculated: + raise ValueError(f"校验和不匹配: 预期 {SU_calculated:02X}, 实际 {SU_received:02X}") + + # 返回解析的结果 + return { + "device_code": device_id, + "gas_value": int(gas_concentration), + "laser_intensity_level": JJ, + "checksum_valid": SU_received == SU_calculated + } + + +# 业务处理层:解析气体数据、保存数据库、推送数据 +class GasDataHandler: + + alarm_dict = { + 'alarmType': '1', + 'alarmContent': '甲烷浓度超限', + } + + def __init__(self, main_loop=None): + self.last_push = {} + self.alarm_record_center = AlarmRecordCenter(main_loop=main_loop) + + async def handle_data(self, data: bytes): + try: + res = parse_gas_data(data) + if res: + logger.info(f"解析甲烷数据:{format_bytes(data)} {res}") + async for db in get_db(): + data_gas_service = DataGasService(db) + data_gas = DataGas( + device_code=res['device_code'], + gas_value=res['gas_value'] + ) + await data_gas_service.add_data_gas(data_gas) + self.push_data(data_gas) + self.handle_alarm(data_gas) + except Exception as e: + logger.exception(f"处理甲烷数据出错: {e}") + + def push_data(self, data_gas): + global_config = GlobalConfig() + gas_push_config = global_config.get_gas_push_config() + if gas_push_config and gas_push_config.push_url and gas_push_config.push_interval > 0: + last_ts = self.last_push.get(data_gas.device_code) + current_time = datetime.now() + + # 检查是否需要推送数据 + if last_ts is None or (current_time - last_ts).total_seconds() > gas_push_config.push_interval: + send_data = json.loads(copy.deepcopy(data_gas.json())) + send_data.pop("id") + asyncio.create_task(send_request_async(gas_push_config.push_url, send_data)) + self.last_push[data_gas.device_code] = current_time # 更新推送时间戳 + + def handle_alarm(self, data_gas): + if data_gas.gas_value > 100: + logger.info(f"甲烷浓度超限报警: {data_gas}") + self.alarm_record_center.upload_alarm_record(device_code=data_gas.device_code, + alarm_dict=self.alarm_dict, + alarm_value=data_gas.gas_value) + else: + logger.info(f"甲烷浓度正常: {data_gas}") \ No newline at end of file diff --git a/tcp/harmful_device_handler.py b/tcp/harmful_device_handler.py index ecbdd9c..dd48070 100644 --- a/tcp/harmful_device_handler.py +++ b/tcp/harmful_device_handler.py @@ -101,10 +101,10 @@ # 判断异常并上报 if gas_type is not None and gas_data is not None: if self._is_data_alarm(device_code, gas_type, gas_data): - print(f"报警: {device_code}, {gas_type}, {gas_data}") + print(f"四合一浓度报警: {device_code}, {gas_type}, {gas_data}") self._save_and_send_alarm(device_code, gas_type, gas_data) - print(self._harmful_gas_manager.get_device_all_data(device_code)) + print(f'更新四合一{device_code}数据 {self._harmful_gas_manager.get_device_all_data(device_code)}') except json.JSONDecodeError: logger.error(f"JSON解析错误: {message}") @@ -197,10 +197,7 @@ encoded_bytes = base64.b64encode(message.encode('utf-8')) # 将字节编码结果转换为字符串 encoded_string = encoded_bytes.decode('utf-8') - logger.debug(f'before encode: {message}') - logger.debug(f'after encode: {encoded_string}') push_message = {"content": encoded_string} - logger.debug(f'body: {push_message}') asyncio.create_task(send_request_async(harmful_push_config.push_url, push_message)) self._push_ts_dict[device_code] = current_time # 更新推送时间戳 else: diff --git a/tcp/tcp_client_connector.py b/tcp/tcp_client_connector.py index 6e0b776..1fb81e9 100644 --- a/tcp/tcp_client_connector.py +++ b/tcp/tcp_client_connector.py @@ -13,7 +13,9 @@ from services.data_gas_service import DataGasService from services.global_config import GlobalConfig - +''' +这个文件目前没有用了 +''' def parse_gas_data(data): # 数据长度检查,确保最小长度符合协议要求 if len(data) < 13: diff --git a/tcp/tcp_client_manager.py b/tcp/tcp_client_manager.py new file mode 100644 index 0000000..74d9c8d --- /dev/null +++ b/tcp/tcp_client_manager.py @@ -0,0 +1,48 @@ +import asyncio +from typing import List, Dict + +from common.consts import DEVICE_TYPE, NotifyChangeType, TREE_COMMAND +from db.database import get_db +from entity.device import Device +from services.device_service import DeviceService + +from common.global_logger import logger +from tcp.gas_device_handler import GasDataHandler +from tcp.tcp_connection import TcpConnection + + +# TcpClientManager 负责管理各设备的 TcpConnection,并提供对外接口 +class TcpClientManager: + def __init__(self, device_service, main_loop=None): + self.main_loop = main_loop + self.device_service = device_service + self.connector_map = {} # device_id -> TcpConnection + + async def start(self): + """从数据库加载设备并连接所有设备""" + devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 + logger.info(f"get {len(devices)} tree devices") + for device in devices: + await self.start_device_connect(device) + + async def start_device_connect(self, device): + if device.id in self.connector_map: + logger.warning(f"设备 {device.id} 已连接") + return + connector = TcpConnection(ip=device.gas_ip, port=333) + asyncio.create_task(connector.connection_monitor()) + handler = GasDataHandler(main_loop=self.main_loop) + connector.register_data_handler(handler.handle_data) + self.connector_map[device.id] = connector + asyncio.create_task(connector.connect()) + # 启动定时查询任务(查询间隔由设备配置决定) + asyncio.create_task( + connector.start_periodic_query(query_command=TREE_COMMAND.GAS_QUERY, interval=3)) + + async def send_message_to_device(self, device_id: int, message: bytes, have_response: bool = True): + if device_id not in self.connector_map: + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + connector = self.connector_map.get(device_id) + if connector: + await connector.send_message(message, have_response=have_response) diff --git a/tcp/tcp_client_manager.py.bak b/tcp/tcp_client_manager.py.bak new file mode 100644 index 0000000..22a0d89 --- /dev/null +++ b/tcp/tcp_client_manager.py.bak @@ -0,0 +1,89 @@ +import asyncio +from typing import List, Dict + + +from common.consts import DEVICE_TYPE, NotifyChangeType +from db.database import get_db +from entity.device import Device +from services.device_service import DeviceService + +from common.global_logger import logger +from services.global_config import GlobalConfig +from tcp.tcp_client_connector import TcpClientConnector + + +class TcpClientManager: + def __init__(self, device_service: DeviceService): + self.devices: List[Device] = [] + self.connector_map: Dict[int, TcpClientConnector] = {} + + self.device_service = device_service + + # 注册设备和模型的变化回调 + # self.device_service.register_change_callback(self.on_device_change) + + async def load_and_connect_devices(self): + """从数据库加载设备并连接所有设备""" + devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 + logger.info(f"get {len(devices)} tree devices") + for device in devices: + await self.start_device_connect(device) + + async def start(self): + """启动设备管理器""" + await self.load_and_connect_devices() + + async def start_device_connect(self, device: Device): + if device and int(device.type) == DEVICE_TYPE.TREE and device.gas_ip: + if device.id in self.connector_map: + logger.warning(f"Device {device.id} is already connected.") + return # 防止重复连接 + connector = TcpClientConnector(ip=device.gas_ip, port=333) + self.connector_map[device.id] = connector + asyncio.create_task(connector.connect()) + + async def stop_device_connect(self, device_id): + if device_id in self.connector_map: + connector = self.connector_map.pop(device_id) + await self.disconnect_device(connector) + + async def restart_device_thread(self, device_id): + await self.stop_device_connect(device_id) + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + + async def on_device_change(self, device_id, change_type): + """设备变化时的回调处理""" + if change_type == NotifyChangeType.DEVICE_CREATE: + # 新增设备,加载新设备并连接 + new_device = await self.device_service.get_device(device_id) + await self.start_device_connect(new_device) + + elif change_type == NotifyChangeType.DEVICE_DELETE: + await self.stop_device_connect(device_id) + + elif change_type == NotifyChangeType.DEVICE_UPDATE: + # 更新设备信息,重新连接 + await self.restart_device_thread(device_id) + + async def disconnect_device(self, connector: TcpClientConnector): + """断开设备连接""" + await connector.disconnect() + + async def send_message_to_device(self, device_id, message: bytes, have_response): + if device_id not in self.connector_map: + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + connector = self.connector_map[device_id] + if connector: + await connector.send_message(message, have_response=have_response) + + + +# if __name__ == '__main__': + # async for db in get_db(): + # global_config = GlobalConfig() + # await global_config.init_config() + # device_service = DeviceService(db) + # tcp_manager = TcpManager(device_service) + # asyncio.run(tcp_manager.start()) diff --git a/tcp/tcp_connection.py b/tcp/tcp_connection.py new file mode 100644 index 0000000..1dd433d --- /dev/null +++ b/tcp/tcp_connection.py @@ -0,0 +1,148 @@ +import asyncio +from common.global_logger import logger + + +# TcpConnection 类只负责连接、重连、收发消息,同时维护一个消息队列 +class TcpConnection: + def __init__(self, ip: str, port: int, timeout: float = 5, reconnect_interval: float = 3): + self.ip = ip + self.port = port + self.timeout = timeout + self.reconnect_interval = reconnect_interval + self.reader = None + self.writer = None + self.is_connected = False + self.data_handler = None # 上层业务处理回调 async function(data: bytes) + self.send_lock = asyncio.Lock() + self.read_lock = asyncio.Lock() + self.message_queue = asyncio.Queue() # 存放待发送的消息元组 (message, have_response) + self.response_queue = asyncio.Queue() # 存放所有读取到的响应数据 + self._read_task = None + self._message_task = None + + async def connect(self): + while not self.is_connected: + try: + logger.info(f"正在连接 {self.ip}:{self.port}") + self.reader, self.writer = await asyncio.wait_for( + asyncio.open_connection(self.ip, self.port), + timeout=self.timeout + ) + + # 验证连接是否真正建立 + if self.writer is None or self.writer.is_closing(): + raise ConnectionError("连接未能成功建立") + + self.is_connected = True + logger.info(f"已连接 {self.ip}:{self.port}") + + # 取消可能遗留的任务,再启动新的后台任务 + if self._read_task is not None: + self._read_task.cancel() + if self._message_task is not None: + self._message_task.cancel() + self._read_task = asyncio.create_task(self._read_loop()) + self._message_task = asyncio.create_task(self._process_message_queue()) + except Exception as e: + logger.error(f"连接 {self.ip}:{self.port} 失败: {e},{self.reconnect_interval}s后重试") + await asyncio.sleep(self.reconnect_interval) + + async def connection_monitor(self): + """外部启动的连接监控任务,负责在连接断开时重连""" + while True: + if not self.is_connected: + await self.connect() + await asyncio.sleep(1) # 根据需要调整检测间隔 + + async def disconnect(self): + if self.writer: + self.writer.close() + try: + await self.writer.wait_closed() + except Exception as e: + logger.error(f"关闭连接异常: {e}") + self.reader = None + self.writer = None + self.is_connected = False + # 取消后台任务,防止它们继续访问已断开的连接 + if self._read_task: + self._read_task.cancel() + self._read_task = None + if self._message_task: + self._message_task.cancel() + self._message_task = None + logger.info(f"断开连接 {self.ip}:{self.port}") + + async def _read_loop(self): + while self.is_connected: + # 如果 self.reader 为 None,等待后再重试 + if self.reader is None: + await asyncio.sleep(0.1) + continue + try: + async with self.read_lock: + data = await asyncio.wait_for(self.reader.read(1024), timeout=self.timeout) + if data: + logger.info(f"从 {self.ip}:{self.port} 收到数据: {data}") + await self.response_queue.put(data) + if self.data_handler: + await self.data_handler(data) + # else: + # logger.warning("未收到数据,断开连接") + # await self.disconnect() + # await self.connect() + except Exception as e: + logger.exception(f"读取数据出错 {self.ip}:{self.port}: {e}") + await self.disconnect() + # await self.connect() + + async def _process_message_queue(self): + while self.is_connected: + message, have_response = await self.message_queue.get() + await self._send_message_with_retry(message, have_response) + + async def _send_message_with_retry(self, message: bytes, have_response: bool): + try: + async with self.send_lock: + if not self.is_connected: + await self.connect() + self.writer.write(message) + await self.writer.drain() + logger.info(f"向 {self.ip}:{self.port} 发送消息: {message}") + if have_response: + # 等待统一读取任务读取到响应 + response = await asyncio.wait_for(self.response_queue.get(), timeout=self.timeout) + return response + # if have_response and self.data_handler: + # async with self.read_lock: + # data = await asyncio.wait_for(self.reader.read(1024), timeout=self.timeout) + # await self.data_handler(data) + except Exception as e: + logger.exception(f"发送消息失败: {e}") + # 重新入队,等待重连后再次发送 + await self.message_queue.put((message, have_response)) + await self.disconnect() + # await self.connect() + + async def send_message(self, message: bytes, have_response: bool = True): + """将消息放入发送队列""" + await self.message_queue.put((message, have_response)) + logger.info(f"消息已加入队列: {message}") + + def register_data_handler(self, handler): + """注册处理接收数据的回调函数,handler 应为 async function(data: bytes)""" + self.data_handler = handler + + async def start_periodic_query(self, query_command: bytes, interval: float): + """自动定时发送查询指令,无论连接状态如何均持续执行""" + while True: + if not self.is_connected: + logger.info("当前未连接,等待重连...") + await asyncio.sleep(1) + continue + try: + # 将查询消息也放入队列,确保顺序一致 + await self.send_message(query_command, have_response=True) + except Exception as e: + logger.error(f"定时查询发送失败: {e}") + await asyncio.sleep(interval) diff --git a/algo/scene_runner.py b/algo/scene_runner.py index 5120f96..27ebc3c 100644 --- a/algo/scene_runner.py +++ b/algo/scene_runner.py @@ -14,7 +14,7 @@ from services.device_scene_relation_service import DeviceSceneRelationService from services.device_service import DeviceService from services.scene_service import SceneService -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class SceneRunner: @@ -22,7 +22,7 @@ device_service: DeviceService, scene_service: SceneService, relation_service: DeviceSceneRelationService, - tcp_manager: TcpManager, + tcp_manager: TcpClientManager, main_loop): self.device_service = device_service self.scene_service = scene_service diff --git a/app_instance.py b/app_instance.py index c48d657..275efd4 100644 --- a/app_instance.py +++ b/app_instance.py @@ -18,7 +18,7 @@ from services.scene_service import SceneService from services.schedule_job import start_scheduler from tcp.harmful_device_handler import HarmfulGasHandler -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from tcp.tcp_server import TcpServer # from tcp.tcp_server import start_server @@ -51,9 +51,9 @@ app.state.scene_service = scene_service app.state.scene_relation_service = scene_relation_service - tcp_manager = TcpManager(device_service=device_service) + tcp_manager = TcpClientManager(device_service=device_service, main_loop=main_loop) app.state.tcp_manager = tcp_manager - # await tcp_manager.start() + await tcp_manager.start() algo_runner = AlgoRunner( device_service=device_service, @@ -78,8 +78,8 @@ tcp_server.register_data_callback(harmful_handler.parse) # await tcp_server.start() main_loop.create_task(tcp_server.start()) - # main_loop.create_task(start_server()) + # main_loop.create_task(start_server()) main_loop.create_task(start_scheduler()) yield # 允许请求处理 diff --git a/common/global_logger.py b/common/global_logger.py index 47e41b7..ce36104 100644 --- a/common/global_logger.py +++ b/common/global_logger.py @@ -20,7 +20,9 @@ when='midnight', # 每天午夜滚动 interval=1 # 滚动间隔为1天 ) -formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +formatter = logging.Formatter( + '%(asctime)s - %(levelname)-8s - [%(module)-20s.%(funcName)-20s] - %(message)s' +) handler.setFormatter(formatter) # 将handler添加到日志器 diff --git a/db/safe-algo-pro.db b/db/safe-algo-pro.db index 966beaa..3849239 100644 --- a/db/safe-algo-pro.db +++ b/db/safe-algo-pro.db Binary files differ diff --git a/scene_handler/alarm_message_center.py b/scene_handler/alarm_message_center.py index 95434bb..a1f4730 100644 --- a/scene_handler/alarm_message_center.py +++ b/scene_handler/alarm_message_center.py @@ -4,6 +4,8 @@ from collections import deque from threading import Thread, Lock +from common.global_logger import logger + ''' 队列消息取出规则: - 按 alarmCategory和 category_order 从小到大排序。 @@ -38,7 +40,7 @@ message = copy.deepcopy(message_ori) message['timestamp'] = int(time.time()) # 添加消息放入队列的时间 with self.lock: - print(message) + # print(message) self.queue.append(message) # 动态更新优先级映射 @@ -55,7 +57,7 @@ now = time.time() with self.lock: self.queue = deque([msg for msg in self.queue if now - msg['timestamp'] <= self.retention_time]) - print(f'清理后的队列长度: {len(self.queue)}') + logger.debug(f'清理后的队列长度: {len(self.queue)}') # def _get_next_message(self): # """按优先级和时间规则取出下一条消息""" diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py index 5451a16..dbc144d 100644 --- a/scene_handler/base_scene_handler.py +++ b/scene_handler/base_scene_handler.py @@ -8,15 +8,15 @@ from ultralytics import YOLO -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager class BaseSceneHandler: def __init__(self, device: Device, thread_id: str, - tcp_manager: TcpManager, - main_loop ): + tcp_manager: TcpClientManager, + main_loop): self.device = device self.thread_id = thread_id self.tcp_manager =tcp_manager diff --git a/scene_handler/block_scene_handler.py b/scene_handler/block_scene_handler.py index a365713..2d51b22 100644 --- a/scene_handler/block_scene_handler.py +++ b/scene_handler/block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -91,7 +91,7 @@ }, { 'alarmCategory': 0, - 'alarmType': '18', + 'alarmType': '2', 'handelType': 2, 'category_order': -1, 'class_idx': [18], @@ -103,25 +103,25 @@ }, { 'alarmCategory': 0, - 'alarmType': '19', # todo + 'alarmType': '6', 'handelType': 4, 'category_order': -1, 'class_idx': [4], 'alarm_name': 'cigarette', 'alarmContent': '吸烟', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', 'label': '吸烟', 'model_type': 'safe', }, { 'alarmCategory': 0, - 'alarmType': '2', - 'handelType': 4, # todo + 'alarmType': '24', + 'handelType': 4, 'category_order': -1, 'class_idx': [5], 'alarm_name': 'phone', 'alarmContent': '打电话', - 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', # todo + 'alarmSoundMessage': b'', # todo 'label': '打电话', 'model_type': 'safe', }, @@ -183,7 +183,7 @@ def get_group_device_list(device_code): health_device_codes = [] harmful_device_codes = [] - url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?devcode={device_code}' + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' response = get_request(url) if response and response.get('code') == 200 and response.get('data'): data = response.get('data') @@ -196,7 +196,7 @@ class BlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} diff --git a/scene_handler/helmet_data_processor.py b/scene_handler/helmet_data_processor.py index 740e88f..5d19c54 100644 --- a/scene_handler/helmet_data_processor.py +++ b/scene_handler/helmet_data_processor.py @@ -17,7 +17,7 @@ }, 'health_heartrate': { 'alarmCategory': 2, - 'alarmType': '18', + 'alarmType': '19', 'handelType': 3, 'category_order': -1, 'alarm_name': 'health_alarm', diff --git a/scene_handler/internet_limit_space_scene_handler.py b/scene_handler/internet_limit_space_scene_handler.py new file mode 100644 index 0000000..cf9199e --- /dev/null +++ b/scene_handler/internet_limit_space_scene_handler.py @@ -0,0 +1,1228 @@ +import asyncio +import base64 +import traceback +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy, copy + +import time +from typing import Dict, List + +import cv2 +from datetime import datetime +import csv + +from algo.stream_loader import OpenCVStreamLoad +from common.device_status_manager import DeviceStatusManager +from common.global_logger import logger +from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager +from common.http_utils import send_request, get_request +from common.image_plotting import Annotator +from entity.device import Device +import numpy as np +from ultralytics import YOLO + +from scene_handler.alarm_message_center import AlarmMessageCenter +from scene_handler.alarm_record_center import AlarmRecordCenter +from scene_handler.base_scene_handler import BaseSceneHandler +from scene_handler.helmet_data_processor import HelmetDataProcessor +from services.global_config import GlobalConfig +from tcp.tcp_client_manager import TcpClientManager + + +def create_value_iterator(values): + for value in values: + yield value + + +fake_list = [ # 假如这是你从四合一后台请求来的数据 + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:49'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:51'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:52'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, + {"data": {'ch4': '0.00', 'co': '0.00', 'h2s': '0.00', 'id': '142913', + 'logtime': '2025-01-14 15:40:49', 'o2': '15.90', 'uptime': '2026-01-14 15:40:53'}}, +] +value_iterator = create_value_iterator(fake_list) + +COLOR_RED = (0, 0, 255) +COLOR_BLUE = (255, 0, 0) + + +def flatten(lst): + result = [] + for i in lst: + if isinstance(i, list): + result.extend(flatten(i)) # 递归调用以处理嵌套列表 + else: + result.append(i) + return result + + +''' +alarmCategory: +0 劳保用品检测异常:三脚架、灭火器、鼓风机、指示牌、面罩、交底 +1 作业过程隐患:闲杂人、安全帽、打电话、吸烟、袖标 +2 人员健康异常 +3 气体浓度异常 +4 上中下气体浓度异常 ? + +handelType: +0 检测到报警 +1 未检测到报警 +2 人未穿戴报警 +3 其他 +4 人员检测到报警 + +''' +ALARM_DICT = { + 'no_brief': { + 'alarmCategory': 0, + 'alarmType': '20', + 'handelType': 1, + 'category_order': 6, + 'alarm_name': 'no_brief', + 'alarmContent': '未进行施工交底', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_tripod': { + 'alarmCategory': 0, + 'alarmType': '21', + 'handelType': 1, + 'category_order': 1, + 'alarm_name': 'no_tripod', + 'alarmContent': '未检测到三脚架', + 'alarmSoundMessage': b'', # todo + 'label': '', + }, + 'no_mask': { + 'alarmCategory': 0, + 'alarmType': '11', + 'handelType': 1, + 'category_order': 5, + 'alarm_name': 'no_mask', + 'alarmContent': '未佩戴呼吸防护设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x12\x00\xA6', + 'label': '' + }, + 'no_blower': { + 'alarmCategory': 0, + 'alarmType': '13', + 'handelType': 1, + 'category_order': 3, + 'alarm_name': 'no_blower', + 'alarmContent': '没有检测到通风设备', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x1A\x00\xAE', + 'label': '没有检测到通风设备' + }, + 'no_extinguisher': { + 'alarmCategory': 0, + 'alarmType': '14', + 'handelType': 1, + 'category_order': 2, + 'alarm_name': 'no_fire_extinguisher', + 'alarmContent': '未检测到灭火器', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x30\x00\xC4', + 'label': '', + }, + 'no_board': { + 'alarmCategory': 0, + 'alarmType': '17', + 'handelType': 1, + 'category_order': 4, + 'alarm_name': 'no_board', + 'alarmContent': '未检测到指示牌', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x33\x00\xC7', + 'label': '', + }, + 'harmful_gas': { + 'alarmCategory': 3, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'harmful_alarm', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'umd_harmful_gas': { # todo 要跟上面区分吗 + 'alarmCategory': 4, + 'alarmType': '', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'umd_harmful_gas', + 'alarmContent': '有害气体浓度超标', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x35\x00\xC9', + 'label': '', + }, + 'health': { + 'alarmCategory': 2, + 'alarmType': '18', + 'handelType': 3, + 'category_order': -1, + 'alarm_name': 'health_alarm', + 'alarmContent': '作业人员心率血氧异常', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x36\x00\xCA', + 'label': '', + }, + 'smoke': { + 'alarmCategory': 1, + 'alarmType': '6', + 'handelType': 4, + 'category_order': 4, + 'alarm_name': 'cigarette', + 'alarmContent': '吸烟', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x03\x00\x97', + 'label': '吸烟', + }, + 'phone': { + 'alarmCategory': 1, + 'alarmType': '24', + 'handelType': 4, + 'category_order': 3, + 'alarm_name': 'phone', + 'alarmContent': '打电话', + 'alarmSoundMessage': b'', # todo + 'label': '打电话', + }, + 'aqm': { + 'alarmCategory': 1, + 'alarmType': '2', + 'handelType': 2, + 'category_order': 2, + 'alarm_name': 'no_helmet', + 'alarmContent': '未佩戴安全帽', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x01\x00\x95', + 'label': '未佩戴安全帽', + }, + 'armband': { + 'alarmCategory': 1, + 'alarmType': '23', + 'handelType': 2, + 'category_order': 5, + 'alarm_name': 'no_armband', + 'alarmContent': '未佩戴袖标', + 'alarmSoundMessage': b'', # todo + 'label': '未佩戴袖标', + 'model_type': 'safe', + }, + 'break': { + 'alarmCategory': 1, + 'alarmType': '3', + 'handelType': 2, + 'category_order': 1, + 'alarm_name': 'break_in_alarm', + 'alarmContent': '非法闯入', + 'alarmSoundMessage': b'\xaa\x01\x00\x93\x37\x00\xCB', + 'label': '非法闯入', + }, +} + +# UMD_PASS_MESSAGE = b'' # 上中下气体检测通过 +PREPARE_COMPLETE_MESSAGE = b'\xaa\x01\x00\x93\x19\x00\xAD' # 满足有限空间作业要求,可以作业 + + +def writeFile(file_path, data): + # print(f"写入{data}") + with open(file_path, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerows(data) + + +class EventController(): + def __init__(self): + self.timeout_event = asyncio.Event() + self.umd_complete = asyncio.Event() + self.qianzhi_check_complete = asyncio.Event() + self.laobao_complete = asyncio.Event() + + +class SiHeYi(): + def __init__(self, harmful_device_code, harmful_data_manager): + self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url + self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager + self.last_ts = None # 上次读取的数据的生成时间戳 + + def waitPowerOn(self, script_start_time): + """ + 阻塞函数 + 循环是否开机,只有检测到开机才会退出函数 + + :param script_start_time:脚本启动的时间戳 + + :return: + """ + print("检测四合一是否开机") + + while True: + self.getNewDataRemote() + flag = script_start_time < self.last_ts # 当前时间T/F 开机/未开机 + print(f'{script_start_time} {self.last_ts} {flag}') + if flag: + print("检测到开机") + return + else: + print("未开机") + time.sleep(2) + + def getNewData(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: + url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={self.harmful_device_code}' + print("访问四合一数据...") + response = get_request(url) + # response = getGasGata_fake() + if response and response.get('data'): + + data = response.get('data') + print(f"访问到四合一数据: {data}") + uptime = datetime.strptime(data.get('uptime'), "%Y-%m-%d %H:%M:%S") + if self.last_ts is None or (uptime.timestamp() - self.last_ts) > 0: + self.last_ts = uptime.timestamp() + if time.time() - uptime.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = data.get('ch4') + co = data.get('co') + h2s = data.get('h2s') + o2 = data.get('o2') + return ch4, co, h2s, o2 + else: + print('ignore') + else: # url没有返回数据 + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def isDataNormal(self, ch4, co, h2s, o2): + """ + 判断四项气体是否正常 + :param ch4: + :param co: + :param h2s: + :param o2: + :return: + """ + if float(ch4) > 10.0 \ + or float(co) > 10.0 \ + or float(h2s) > 120.0 \ + or float(o2) < 15: + return False # 气体异常 + else: + return True # 气体正常 + + +# class AnQuanMao(): +# def __init__(self, helmet_code): +# self.helmet_code = helmet_code +# self.url = f'http://111.198.10.15:22006/emergency/harmfulData?devcode={helmet_code}' # 后台访问数据的url +# self.last_ts = None # 上次读取的数据的生成时间戳 +# +# def getNewData(self): +# """ +# 阻塞进程 +# :return: +# """ +# while True: +# header = { +# 'ak': 'fe80b2f021644b1b8c77fda743a83670', +# 'sk': '8771ea6e931d4db646a26f67bcb89909', +# } +# url = f'https://jls.huaweisoft.com//api/ih-log/v1.0/ih-api/helmetInfo/{self.helmet_code}' +# print("访问心率血氧数据...") +# response = get_request(url, headers=header) +# if response and response.get('data'): +# print("访问到心率血氧数据") +# vitalsigns_data = response.get('data').get('vitalSignsData') # 访问而来的数据 +# if vitalsigns_data: # 访问成功 +# upload_timestamp = datetime.strptime(vitalsigns_data.get('uploadTimestamp'), +# "%Y-%m-%d %H:%M:%S") # 访问数据的时间 +# if self.last_ts is None or ( +# upload_timestamp.timestamp() - self.last_ts) > 0: # 如果这次访问是第一次访问 或者 访问数据的时间晚于上次时间的数据 +# self.last_ts = upload_timestamp.timestamp() # 更新数据 +# if time.time() - upload_timestamp.timestamp() < 10 * 60: # 访问到的数据是 10分钟内的数据 +# return vitalsigns_data.get('bloodOxygen'), vitalsigns_data.get('heartRate') +# else: +# print("无法访问到心率血氧数据") +# time.sleep(5) +# +# def isDataNormal(self, blood_oxygen, heartrate): +# if heartrate < 60 or heartrate > 120 or blood_oxygen < 85: # 心率和血氧异常 +# return False +# else: +# return True + + +class Laobaocheck(): + def __init__(self, eventController=None, alarm=None): + self.laobao_model = YOLO("weights/labor-v8-20241114.pt") + self.jiaodi_model = YOLO("weights/jiaodi.pt") + self.target = {"三脚架": [0], "灭火器": [34], "鼓风机": [58], "面罩": [11], "工作指示牌": [4, 6, 16]} + self.target_flag = {"三脚架": False, "灭火器": False, "鼓风机": False, "面罩": False, + "工作指示牌": False} # OD 模型有无检测这些目标 + self.jiaodi_flag = False # 分类模型 有无检测到交底 + self.laobao_pool = {} + + self.eventController = eventController + self.alarm = alarm + + def getUndetectedTarget(self): + # 获取未检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == False: + result.append(name) + if not self.jiaodi_flag: + result.append("交底") + return result + + def name2alarm(self, target_name): + alarm_map = { + '三脚架': 'no_tripod', + '灭火器': 'no_extinguisher', + '鼓风机': 'no_blower', + '面罩': 'no_mask', + '工作指示牌': 'no_board', + '交底': 'no_brief' + } + return alarm_map.get(target_name, None) + + def getDetectedTarget(self): + # 获取已检测目标的名称,返回str列表 + result = [] + for name, flag in self.target_flag.items(): + if flag == True: + result.append(name) + return result + + def name2id(self, input): + # 检测名称 映射为 id + if isinstance(input, str): + return self.target[input] + elif isinstance(input, list): + result = [] + for item in input: + if item in self.target: + result.append(self.target[item]) + if len(result) == 0: return [] + return list(set(np.concatenate([r for r in result]).astype(int).tolist())) + + def id2name(self, input): + """ + + :param input: int 或 [int,int,int...] + :return: + """ + # id -> 类别名称 + result = [] + if isinstance(input, int): + input = [input] + for id in input: + for k, v in self.target.items(): # k: 类别名称 , v: id_list + if id in v: result.append(k) + + return list(set(result)) + + def predict_isJiaodi(self, frames): + """ + 调用 jiaodi.pt 分类模型 + :return: True:交底,False:没检测到 交底 + """ + jiaodi_results = self.jiaodi_model.predict(source=frames, save=False, verbose=False) + jiaodi_prob = [jiaodi_result.probs.data[0].item() for jiaodi_result in jiaodi_results] + for prob in jiaodi_prob: + if prob > 0.6: + return True + return False + + def predict_laobao(self, frames): + ''' + 调用 labor-v8-20241114.pt OD 模型 + :param frames: + :return: [类别1,类别2] + ''' + target_idx = self.name2id(self.getUndetectedTarget()) + results = self.laobao_model.predict(source=frames, classes=flatten(target_idx), conf=0.6, + save=False, verbose=False) # results:list(4) 4帧的检测结果 + pred_c_list = list(set(np.concatenate([result.boxes.cls.tolist() for result in results]).astype( + int).tolist())) # 检测到的目标类别id_list,已去重 + return self.id2name(pred_c_list) + + def updateUnpredictedTargets(self, jiaodi_flag, pred_labels): + ''' + 更新 已检测到的目标 列表 和有无检测到交底 + :param pred_labels: [str, str...] + :return: None + ''' + for pred_label in pred_labels: + print(f"检测到{pred_label}") + self.target_flag[pred_label] = True + if self.jiaodi_flag == False and jiaodi_flag == True: + print(f"劳保检测:检测到交底") + self.jiaodi_flag = jiaodi_flag + + def model_predict_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + break + cv2.namedWindow("Video Frame", cv2.WINDOW_AUTOSIZE) + cv2.resizeWindow('Video Frame', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame", frames[0]) + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cap.release() + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + if self.eventController != None and self.eventController.timeout_event.is_set(): # 超时退出 + cap.release() + cv2.destroyAllWindows() + return + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController != None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if self.getUndetectedTarget() == []: # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + cap.release() + cv2.destroyAllWindows() + return # 退出检测 + else: # 如果还有未检测到的 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict = self.name2alarm(target_name) + if alarm_dict: + self.alarm.addAlarm(alarm_dict) + cap.release() + cv2.destroyAllWindows() + + def model_predict(self, stream_loader): + for frames in stream_loader: # type : list (4),连续的4帧 + if self.eventController is not None and self.eventController.timeout_event.is_set(): # 超时退出 + return + + if not frames: + continue + + # 构造 要检测目标的 id_list(把之前检测的目标 从 要检测目标的集合移出) + jiaodi_flag = self.predict_isJiaodi(frames) # bool, 检测 新收集的这几帧有无交底 + pred_label = self.predict_laobao(frames) # [str, str...] + self.updateUnpredictedTargets(jiaodi_flag, pred_label) + + if self.eventController is not None and self.eventController.umd_complete.is_set(): # 上中下气体检测完毕 + + # 检验所有物体都检验到了吗 + if not self.getUndetectedTarget(): # 如果全部检验到了 + print("劳保物品 通过") + self.eventController.laobao_complete.set() + return # 退出检测 + else: # 如果还有未检测到的 + # todo 这里是否要生成报警记录 + undetectedTargets = self.getUndetectedTarget() + for target_name in undetectedTargets: + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) + + +class YinHuanCheck: + def __init__(self, device_code, eventController, alarm, frame_threshold=20, batch_size=1): + # 初始化YOLO模型及其他参数 + self.model = YOLO("weights/yinhuan.pt") + self.eventController = eventController + self.alarm = alarm + self.device_code = device_code + self.frame_threshold = frame_threshold # 连续异常帧阈值 + self.batch_size = batch_size + + # 针对每个人的异常计数器,键为 person_id + self.counters = { + 'no_helmet': {}, # 未佩戴安全帽 + 'smoking': {}, # 吸烟(检测到烟头) + 'phone': {}, # 打电话(检测到电话) + 'illegal_intrusion': {} # 非法闯入(既没有安全帽也没有工服) + } + # 针对袖标异常为全局条件:如果当前帧中所有人均未检测到袖标,则更新全局计数器 + self.armband_counter = 0 + + def id2name(self, input_id): + """ + 将检测到的类别ID转换为类别名称,支持单个ID或ID列表 + """ + result = [] + if isinstance(input_id, int): + input_id = [input_id] + for id in input_id: + for k, v in self.model.names.items(): # k: id , v: 类别名称 + if k == id: + result.append(v) + return list(set(result)) + + def detect_person(self, frames): + """ + 对输入的一批视频帧进行人员检测与跟踪,返回每帧中检测到的人员信息。 + 返回格式:list,每项为字典,键为 person_id,值为 {'crop': 截取的人像, 'box': 人员检测框 [x1,y1,x2,y2]} + """ + if not frames: + return [] + skip = False + for i, f in enumerate(frames): + if f is None: + skip = True + if skip: + return [] + + people_results = self.model.track(source=frames, conf=0.6, classes=[0], + save=False, verbose=False) # 检测人(类别0) + results = [] + for people_result in people_results: + orig_img = people_result.orig_img # 当前帧原图 + person_dict = {} + for person_box in people_result.boxes: + person_id = person_box.id.item() + if person_id: + box = person_box.xyxy.squeeze().tolist() # [x1, y1, x2, y2] + # 截取检测到的人像区域 + cropped_image = orig_img[int(box[1]):int(box[3]), int(box[0]):int(box[2])] + person_dict[person_id] = {'crop': cropped_image, 'box': box} + results.append(person_dict) + return results + + def detect_person_targets(self, people_results): + """ + 针对每个人的图像区域进行目标检测(安全帽、工服、烟头、电话、袖标)。 + 返回格式:list,每项为字典,键为 person_id,值为该人身上检测到的目标字典 {label: xyxy} + """ + # 收集所有人的图像区域 + person_images = [] + for frame_persons in people_results: + for info in frame_persons.values(): + person_images.append(info['crop']) + if len(person_images) == 0: + return [] + # 对所有人的区域进行目标检测 + results = self.model.predict(source=person_images, conf=0.6, classes=[2, 3, 4, 5, 6], + save=False, verbose=False) + person_detect_targets = [] + result_idx = 0 + for frame_persons in people_results: + frame_detection = {} + for person_id in frame_persons.keys(): + person_result = results[result_idx] + detection_dict = {} + for box in person_result.boxes: + label = int(box.cls.item()) + label_name = self.id2name(label) + xyxy = box.xyxy.squeeze().tolist() + # 假设每个box只对应一种类别,直接存入字典 + detection_dict[label_name[0]] = xyxy + frame_detection[person_id] = detection_dict + result_idx += 1 + person_detect_targets.append(frame_detection) + return person_detect_targets + + def annotate_alarm(self, frame, condition, person_box=None, detection=None): + """ + 在报警图片上对异常情况进行标注: + - person_box: 异常人员的检测框 + - detection: 异常物品的检测框,格式为 {label: xyxy} + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + + # 绘制异常物品的检测框(如烟头、电话等) + if detection is not None: + if person_box is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in detection.items(): + # 将 detection 框坐标转换为全局坐标 + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, '', color=COLOR_RED, rotated=False) + else: + # 如果没有人的框,则直接使用 detection 框 + for label, box in detection.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm(self, frame, condition, person_id=None, detection=None, person_box=None): + """ + 当某个异常条件达到连续帧阈值时触发报警: + - condition: 异常类型,取值:'no_helmet', 'smoking', 'phone', 'illegal_intrusion', 'armband' + - 对于人员级别的异常,会标注该人员检测框及异常物品 + - 对于全局异常(armband)直接标注图片 + """ + # 定义异常对应的报警类型(需与系统定义的 ALARM_DICT 对应) + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + # 播报异常语言 + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 + annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) + + # 上传报警图片到后台 + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + + def cleanup_counters(self, current_ids): + """ + 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) + """ + for cond in self.counters: + lost_ids = [pid for pid in self.counters[cond] if pid not in current_ids] + for pid in lost_ids: + del self.counters[cond][pid] + + def process_batch(self, frames): + """ + 对一批视频帧进行处理: + 1. 对每帧进行人员检测及目标检测 + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + """ + # 第一步:检测人员及其区域目标 + people_results = self.detect_person(frames) + if all(not d for d in people_results): + return + person_detect_targets = self.detect_person_targets(people_results) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) + current_ids = set() + for frame_persons in people_results: + current_ids.update(frame_persons.keys()) + + # 针对每一帧处理 + for idx, frame in enumerate(frames): + frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } + # 遍历当前帧的每个检测到的人员 + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + no_helmet = "安全帽" not in detections + smoking = "烟头" in detections + phone = "电话" in detections + illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): + if person_id not in self.counters[cond]: + self.counters[cond][person_id] = 0 + if abnormal: + self.counters[cond][person_id] += 1 + else: + self.counters[cond][person_id] = 0 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) + if self.counters[cond][person_id] >= self.frame_threshold: + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 + + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) + self.armband_counter = 0 + + def main(self, stream_loader): + """ + 主流程:从视频流中获取每一批帧,处理后检测异常并报警 + """ + for frames in stream_loader: # stream_loader 每次返回一批连续帧(例如4帧) + try: + self.process_batch(frames) + except Exception as ex: + traceback.print_exc() + # 记录错误信息 + logger.error(ex) + + def main_fake(self, video_path): + cap = cv2.VideoCapture(video_path) + ret, frames = cap.read() + while True: + try: + ret, frames = cap.read() + frames = [frames] + if not ret: + cap.release() + cv2.destroyAllWindows() + break + cv2.namedWindow("Video Frame2", cv2.WINDOW_AUTOSIZE) + # cv2.resizeWindow('Video Frame2', 800, 600) # 宽度800像素,高度600像素 + cv2.imshow("Video Frame2", frames[0]) + + self.process_batch(frames) + + except Exception as ex: + traceback.print_exc() + logger.error(ex) + cv2.destroyAllWindows() + # 等待1毫秒,检查是否按下了'q'键退出 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + +class Alarm(): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): + self.pool = [] + self.device = device + self.thread_id = thread_id + self.tcp_manager = tcp_manager + self.main_loop = main_loop + self.eventController = eventController + + # self.alarm_interval_dict = {} + # self.alarm_interval = device.alarm_interval + # + # self.socket_interval_dict = {} + # self.socket_interval = device.alarm_interval + # self.socket_retry = 3 + + self.alarm_message_center = AlarmMessageCenter(device.id, main_loop=main_loop, tcp_manager=tcp_manager, + category_interval=30, message_send_interval=3, retention_time=10, + category_priority={0: 0, 4: 1, 3: 2, 2: 3, + 1: 4}) # (优先级:0 > 4 > 3 > 2 > 1) + self.alarm_record_center = AlarmRecordCenter(save_interval=device.alarm_interval, main_loop=main_loop) + + # todo 跟下面的alarm task二选一 + # self.thread_pool = GlobalThreadPool() + # self.thread_pool.submit_task(self.alarm_message_center.process_messages) + + def addAlarm(self, alarm_dict): + if alarm_dict.get('alarmSoundMessage'): + self.alarm_message_center.add_message(alarm_dict) + + # def addAlarm(self, content): + # """ + # 添加一条报警到报警队列中 + # :param content: + # :return: + # """ + # if content in self.pool: + # self.pool.remove(content) + # self.pool.append(content) + + def deleteAlarmOfLaoBao(self): + """ + 删除池子中有关劳保检测的报警 + :return: + """ + + # self.pool = [item for item in self.pool if "劳保" not in item] + + def laobao_condition(msg): + return msg['alarmCategory'] == 0 + + self.alarm_message_center.delete_messages(laobao_condition) + + def deleteAlaramOfUmdGas(self): + """ + 删除池子中有关上中下气体的报警 + :return: + """ + + def umd_condition(msg): + return msg['alarmCategory'] == 4 + + self.alarm_message_center.delete_messages(umd_condition) + + # self.pool = [item for item in self.pool if "劳保" not in item] + + # def main(self): + # while True: + # if self.eventController.timeout_event.is_set(): # 前置条件检查超时 + # self.deleteAlarmOfLaoBao() + # self.deleteAlaramOfUmdGas() + # if len(self.pool) != 0: + # content = self.pool.pop(0) + # print(f"{content},报警队列长度:{len(self.pool)}") + # # self.send_alarm_message("no_jiandu") + # time.sleep(1) + # + # def send_tcp_message(self, message: bytes, have_response=False): + # asyncio.run_coroutine_threadsafe( + # self.tcp_manager.send_message_to_device(device_id=self.device.id, + # message=message, + # have_response=have_response), + # self.main_loop) + + # def send_alarm_message(self, type): + # if self.tcp_manager: + # # if self.socket_interval_dict.get(type) is None \ + # # or (datetime.now() - self.socket_interval_dict.get(type)).total_seconds() > int(self.socket_interval): + # logger.debug("send alarm message %s %s", ALARM_DICT[type]['alarmContent'], + # ALARM_DICT[type]['alarmSoundMessage']) + # self.send_tcp_message(ALARM_DICT[type]['alarmSoundMessage'], have_response=True) + # self.socket_interval_dict[type] = datetime.now() + + +HEALTH_DEVICE_TYPE = '2' # 安全帽设备类型 +HARMFUL_DEVICE_TYPE = '4' # 四合一设备类型 + + +def get_group_device_list(device_code): + health_device_codes = [] + harmful_device_codes = [] + url = f'http://111.198.10.15:22006/v3/device/listGroupDevs?deviceCode={device_code}' + response = get_request(url) + if response and response.get('code') == 200 and response.get('data'): + data = response.get('data') + for item in data: + health_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HEALTH_DEVICE_TYPE] + harmful_device_codes = [item.get('deviceCode', '') for item in data if + item.get('deviceType', '') == HARMFUL_DEVICE_TYPE] + return health_device_codes, harmful_device_codes + + +class InternetLimitSpaceSceneHandler(BaseSceneHandler): + + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): + super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) + + self.start_time = time.time() # 脚本启动时间戳 + print(f'start time = {self.start_time}') + + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) + + self.executor = ThreadPoolExecutor(max_workers=10) + self.loop = asyncio.get_running_loop() + + self.eventController = EventController() + self.alarm = Alarm(device, thread_id, tcp_manager, main_loop, self.eventController) + self.laobao_check = Laobaocheck(self.eventController, self.alarm) + self.yinhuan_check = YinHuanCheck(device_code=device.code, eventController=self.eventController, + alarm=self.alarm) + + self.anQuanMaoList = [] + self.siHeyiList = [] + self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() + health_device_codes, harmful_device_codes = get_group_device_list(device.code) + for health_device_code in health_device_codes: + self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) + for harmful_device_code in harmful_device_codes: + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) + + if self.siHeyiList: + self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 + else: + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) + + async def laobaoCheck_task(self): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + # await loop.run_in_executor(executor, self.laobao_check.model_predict_fake, + # r"D:\workspace\pythonProject\safe-algo-pro\2025-02-25 15-25-48.mkv") + await self.loop.run_in_executor(self.executor, self.laobao_check.model_predict, self.stream_loader) + + async def uMDGasCheck_task(self, eventController=None): + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + tflag_pool = [] # 返回数据正常了几次 + await self.loop.run_in_executor(self.executor, self.siHeyiUmd.waitPowerOn, + self.start_time) # 阻塞 uMDGasCheck_task 协程, 检测不到开机不往后进行 + print('上中下气体检测:四合一已开机') + + while True: # 模拟循环检测气体 + if eventController.timeout_event.is_set(): # 超时退出 + return + + ch4, co, h2s, o2 = await self.loop.run_in_executor(self.executor, self.siHeyiUmd.getNewDataRemote) # 判断气体是否合规 + flag = self.siHeyiUmd.isDataNormal(ch4, co, h2s, o2) + if flag == False: + tflag_pool.clear() + self.alarm.addAlarm(ALARM_DICT['umd_harmful_gas']) + else: + tflag_pool.append(True) + print(f"上中下气体检测正常次数:{tflag_pool}") + if len(tflag_pool) == 3: + break # 退出检测 + + print('上中下气体检测:上中下气体检测通过') # todo 需要语音吗 + self.eventController.umd_complete.set() + return + + async def alarm_task(self): + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.alarm.alarm_message_center.process_messages) + + async def yinhuanCheck_task(self): + """ + 检查有无吸烟、袖标、安全帽、打电话、闲杂人(工服)等(隐含的类别:人,头) + :return: + """ + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + await self.loop.run_in_executor(self.executor, self.yinhuan_check.main, + self.stream_loader) + + async def xinlvCheck_task(self): + + def fun(anQuanMao): + blood_oxygen, heartrate = anQuanMao.getNewData() + if not anQuanMao.isDataNormal(blood_oxygen, heartrate): + self.alarm.addAlarm(ALARM_DICT['health']) + anQuanMao.sendAlarmRecord(blood_oxygen, heartrate) + # + # flag = anQuanMao.isDataNormal(blood_oxygen, heartrate) + # if flag == False: + # self.alarm.addAlarm(ALARM_DICT['health']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for aqm in self.anQuanMaoList: + await self.loop.run_in_executor(self.executor, fun, aqm) + + async def gasCheck(self): + """ + 四合一气体检测 + :return: + """ + + def fun(siHeyi): + ch4, co, h2s, o2 = siHeyi.getNewDataRemote() + flag = siHeyi.isDataNormal(ch4, co, h2s, o2) + if flag == False: + self.alarm.addAlarm(ALARM_DICT['harmful_gas']) + + # executor = ThreadPoolExecutor(max_workers=3) + # loop = asyncio.get_running_loop() + for siHeYi in self.siHeyiList: + await self.loop.run_in_executor(self.executor, fun, siHeYi) + + def run(self): + async def fun(): + try: + self.loop = asyncio.get_running_loop() + + # 添加异常处理 + def handle_task_exception(task): + try: + task.result() # 触发异常(如果有) + except Exception as e: + logger.exception(f"任务 {task.get_name()} 发生异常: {e}") + + # 并行执行任务 + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + alarm_task = asyncio.create_task(self.alarm_task()) + + # 给所有任务添加异常处理 + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) + + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") + + # 删除前面产生的报警 + self.alarm.deleteAlarmOfLaoBao() + self.alarm.deleteAlaramOfUmdGas() + + # 并行执行任务 + print("开始工作") + xinlvCheck_task = asyncio.create_task(self.xinlvCheck_task()) + yinhuanCheck_task = asyncio.create_task(self.yinhuanCheck_task()) + gasCheck_task = asyncio.create_task(self.gasCheck()) + + # 也给这些任务添加异常处理 + for task in [xinlvCheck_task, yinhuanCheck_task, gasCheck_task]: + task.add_done_callback(handle_task_exception) + + try: + results = await asyncio.gather(yinhuanCheck_task, gasCheck_task, xinlvCheck_task, + return_exceptions=True) + for result in results: + if isinstance(result, Exception): + logger.exception(f"任务发生异常: {result}") + except Exception as e: + logger.exception(f"gather 执行过程中发生异常: {e}") + + done1, pending1 = await asyncio.wait({alarm_task}, timeout=300000.0) + except Exception as e: + logger.exception(f"run 方法中的 fun 发生异常: {e}") + + asyncio.run(fun()) + + +if __name__ == '__main__': + # print(getNewGasData()) + model = YOLO("/home/pc/Desktop/project/safe-algo-pro/weights/yinhuan.pt") + print(model.names) diff --git a/scene_handler/intranet_block_scene_handler.py b/scene_handler/intranet_block_scene_handler.py index 953d18a..736941f 100644 --- a/scene_handler/intranet_block_scene_handler.py +++ b/scene_handler/intranet_block_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.limit_space_scene_handler import is_overlapping from services.data_gas_service import DataGasService from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager from entity.device import Device from common.http_utils import get_request @@ -193,7 +193,7 @@ class IntranetBlockSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.__stop_event = Event(loop=main_loop) self.health_ts_dict = {} @@ -219,7 +219,7 @@ for helmet_code in self.health_device_codes: self.thread_pool.submit_task(self.health_data_task, helmet_code) for harmful_device_code in self.harmful_device_codes: - self.thread_pool.submit_task(self.harmful_data_query_task, harmful_device_code) + self.thread_pool.submit_task(self.harmful_data_task, harmful_device_code) self.thread_pool.submit_task(self.alarm_message_center.process_messages) @@ -396,7 +396,6 @@ alarm_dict = [d for d in ALARM_DICT if d['alarmCategory'] == 1 and d['alarm_name'] == 'harmful_alarm'] if alarm_dict: self.alarm_message_center.add_message(alarm_dict[0]) - # todo 需要生成报警记录吗 def model_predict(self, frames): result_boxes = [] diff --git a/scene_handler/intranet_limit_space_scene_handler.py b/scene_handler/intranet_limit_space_scene_handler.py index 73dcbcf..e707dc3 100644 --- a/scene_handler/intranet_limit_space_scene_handler.py +++ b/scene_handler/intranet_limit_space_scene_handler.py @@ -15,6 +15,7 @@ from common.device_status_manager import DeviceStatusManager from common.global_logger import logger from common.global_thread_pool import GlobalThreadPool +from common.harmful_gas_manager import HarmfulGasManager from common.http_utils import send_request, get_request from common.image_plotting import Annotator from entity.device import Device @@ -26,7 +27,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from scene_handler.helmet_data_processor import HelmetDataProcessor from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager def create_value_iterator(values): @@ -151,7 +152,7 @@ }, 'harmful_gas': { 'alarmCategory': 3, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'harmful_alarm', @@ -161,7 +162,7 @@ }, 'umd_harmful_gas': { # todo 要跟上面区分吗 'alarmCategory': 4, - 'alarmType': '', # todo + 'alarmType': '', 'handelType': 3, 'category_order': -1, 'alarm_name': 'umd_harmful_gas', @@ -211,7 +212,7 @@ }, 'armband': { 'alarmCategory': 1, - 'alarmType': '18', # todo + 'alarmType': '20', # todo 'handelType': 2, 'category_order': 5, 'alarm_name': 'no_armband', @@ -252,9 +253,10 @@ class SiHeYi(): - def __init__(self, harmful_device_code): + def __init__(self, harmful_device_code, harmful_data_manager): self.url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={harmful_device_code}' # 后台访问数据的url self.harmful_device_code = harmful_device_code # 四合一标识符 + self.harmful_data_manager = harmful_data_manager self.last_ts = None # 上次读取的数据的生成时间戳 def waitPowerOn(self, script_start_time): @@ -290,6 +292,34 @@ :return: """ while True: + harmful_gas_data = self.harmful_data_manager.get_device_all_data(self.harmful_device_code) + if harmful_gas_data: + latest_gas_ts = max(harmful_gas_data.values(), key=lambda d: d['gas_ts'])['gas_ts'] + if self.last_ts is None or (latest_gas_ts.timestamp() - self.last_ts) > 0: + self.last_ts = latest_gas_ts.timestamp() + if time.time() - latest_gas_ts.timestamp() < 10 * 60: # 10分钟以前的数据不做处理 + ch4 = harmful_gas_data.get(50).get('gas_value') + co = harmful_gas_data.get(4).get('gas_value') + h2s = harmful_gas_data.get(3).get('gas_value') + o2 = harmful_gas_data.get(5).get('gas_value') + return ch4, co, h2s, o2 + else: + print('ignore') + else: + logger.debug("四合一没有读取到数据") + time.sleep(5) + + def getNewDataRemote(self): + """ + 阻塞函数 + 访问后台数据库 读取最新产生的四合一浓度 + 如果有返回数据则记录 该数据产生的时间。 + 如果之前没有记录 数据产生时间 或 访问到的数据产生时间 晚于 上次记录时间: + 则视为读取到新数据,返回新数据 + 没有数据等待n秒后重复询问 + :return: + """ + while True: url = f'http://172.27.46.84:30003/emergency/harmfulData?devcode={self.harmful_device_code}' print("访问四合一数据...") response = get_request(url) @@ -310,7 +340,7 @@ else: print('ignore') else: # url没有返回数据 - print("四合一没有读取到数据") + logger.debug("四合一没有读取到数据") time.sleep(5) def isDataNormal(self, ch4, co, h2s, o2): @@ -552,9 +582,9 @@ # todo 这里是否要生成报警记录 undetectedTargets = self.getUndetectedTarget() for target_name in undetectedTargets: - alarm_dict = self.name2alarm(target_name) - if alarm_dict: - self.alarm.addAlarm(alarm_dict) + alarm_dict_key = self.name2alarm(target_name) + if alarm_dict_key: + self.alarm.addAlarm(ALARM_DICT[alarm_dict_key]) class YinHuanCheck: @@ -691,9 +721,9 @@ if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): alarm_detections = {} if alarm_type == 'smoke': - alarm_detections = {key : detection[key] for key in detection if key == '烟头'} + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} elif alarm_type == 'phone': - alarm_detections = {key : detection[key] for key in detection if key == '电话'} + alarm_detections = {key: detection[key] for key in detection if key == '电话'} # 生成报警图片:在报警图片上对异常人员与异常物品进行标注 annotated_image = self.annotate_alarm(frame, ALARM_DICT[alarm_type]['label'], person_box, alarm_detections) @@ -701,6 +731,55 @@ self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], alarm_np_img=annotated_image) + def annotate_alarm_aggregated(self, frame, condition, candidates, alarm_type): + """ + 对一张报警图片进行标注,候选列表 candidates 是一个列表,每项是字典, + 包含 'person_box'(整图坐标)和 'detection'(相对于裁剪图的检测结果)。 + 在图片上标注所有候选人员及其异常检测框。 + """ + annotator = Annotator(deepcopy(frame), None, 18, "Arial.ttf", False) + for candidate in candidates: + person_box = candidate.get("person_box") + detection = candidate.get("detection") + alarm_detections = {} + if alarm_type == 'smoke': + alarm_detections = {key: detection[key] for key in detection if key == '烟头'} + elif alarm_type == 'phone': + alarm_detections = {key: detection[key] for key in detection if key == '电话'} + + if person_box is not None: + annotator.box_label(person_box, condition, color=COLOR_RED, rotated=False) + if alarm_detections is not None: + offset_x, offset_y = int(person_box[0]), int(person_box[1]) + for label, box in alarm_detections.items(): + global_box = [box[0] + offset_x, box[1] + offset_y, + box[2] + offset_x, box[3] + offset_y] + annotator.box_label(global_box, label, color=COLOR_RED, rotated=False) + else: + if alarm_detections is not None: + for label, box in alarm_detections.items(): + annotator.box_label(box, label, color=COLOR_RED, rotated=False) + return annotator.result() + + def trigger_alarm_aggregated(self, frame, condition, candidates): + """ + 针对一帧内某报警类别的所有候选人员生成一次报警记录和报警图片。 + """ + alarm_mapping = { + 'no_helmet': 'aqm', # 安全帽异常 + 'smoking': 'smoke', # 吸烟异常 + 'phone': 'phone', # 打电话异常 + 'illegal_intrusion': 'break', # 非法闯入 + 'armband': 'armband' # 袖标异常 + } + alarm_type = alarm_mapping.get(condition, 'unknown') + self.alarm.addAlarm(ALARM_DICT[alarm_type]) + if self.alarm.alarm_record_center.need_alarm(self.device_code, ALARM_DICT[alarm_type]): + annotated_image = self.annotate_alarm_aggregated(frame, ALARM_DICT[alarm_type]['label'], candidates, + alarm_type) + self.alarm.alarm_record_center.upload_alarm_record(self.device_code, ALARM_DICT[alarm_type], + alarm_np_img=annotated_image) + def cleanup_counters(self, current_ids): """ 清理各异常计数器中失去的 person id(即当前帧中不再检测到的人员) @@ -714,68 +793,85 @@ """ 对一批视频帧进行处理: 1. 对每帧进行人员检测及目标检测 - 2. 针对每个人判断是否存在异常: - - 未佩戴安全帽:若该人检测结果中没有 "安全帽" - - 吸烟:若检测到 "烟头" - - 打电话:若检测到 "电话" - - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" - 3. 更新每个人针对各异常的连续帧计数器,若达到阈值则触发报警 - 4. 对于袖标异常,若当前帧中所有人均未检测到 "袖标",则更新全局计数器 - 5. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) + 2. 针对每一帧中所有人员判断是否存在异常: + - 未佩戴安全帽:若该人检测结果中没有 "安全帽" + - 吸烟:若检测到 "烟头" + - 打电话:若检测到 "电话" + - 非法闯入:若既没有检测到 "安全帽" 也没有检测到 "工服" + - 袖标异常:若该人检测结果中没有 "袖标" + 3. 对每一帧按报警类别聚合候选人员,若连续异常帧达到阈值,则对该报警类别生成一次报警记录和报警图片 + 4. 清理当前帧中不再检测到的 person id(清除计数器中遗留的记录) """ # 第一步:检测人员及其区域目标 people_results = self.detect_person(frames) - - empty = all(not d for d in people_results) - if empty: + if all(not d for d in people_results): return - person_detect_targets = self.detect_person_targets(people_results) - # 收集当前帧所有检测到的 person id(假设所有帧中检测到的人 id 集合取并集) + # 收集当前帧所有检测到的 person id(各帧检测到的并集) current_ids = set() for frame_persons in people_results: current_ids.update(frame_persons.keys()) - # 针对每一帧分别处理 + # 针对每一帧处理 for idx, frame in enumerate(frames): frame_targets = person_detect_targets[idx] # 当前帧中,各人员的检测结果 + # 用于聚合每个报警类别的候选记录(每项包含 'person_box' 和 'detection') + frame_candidates = { + 'no_helmet': [], + 'smoking': [], + 'phone': [], + 'illegal_intrusion': [] + } # 遍历当前帧的每个检测到的人员 for person_id, detections in frame_targets.items(): - # 定义各异常条件 + person_box = people_results[idx].get(person_id, {}).get('box') no_helmet = "安全帽" not in detections smoking = "烟头" in detections phone = "电话" in detections illegal_intrusion = ("安全帽" not in detections) and ("工服" not in detections) - # 依次更新对应计数器(未佩戴安全帽、吸烟、打电话、非法闯入) - for cond, abnormal in zip(['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], - [no_helmet, smoking, phone, illegal_intrusion]): + for cond, abnormal in zip( + ['no_helmet', 'smoking', 'phone', 'illegal_intrusion'], + [no_helmet, smoking, phone, illegal_intrusion] + ): if person_id not in self.counters[cond]: self.counters[cond][person_id] = 0 if abnormal: self.counters[cond][person_id] += 1 else: self.counters[cond][person_id] = 0 - - # 若连续异常帧数达到阈值,则触发报警 + # 当连续异常帧达到阈值时,将该人员加入对应报警候选列表(每人每类只添加一次) if self.counters[cond][person_id] >= self.frame_threshold: - # 获取该人员的检测框(用于标注) - person_box = people_results[idx][person_id]['box'] - self.trigger_alarm(frame, cond, person_id, detections, person_box) - # 触发报警后重置该人员对应计数器 - self.counters[cond][person_id] = 0 + already_added = any(cand.get("person_id") == person_id for cand in frame_candidates[cond]) + if not already_added: + frame_candidates[cond].append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.counters[cond][person_id] = 0 # 重置计数 - # 针对袖标的全局情况:如果当前帧中所有人均未检测到 "袖标",则更新全局计数器 - # 注意:这里遍历当前帧中每个检测结果 - if not any("袖标" in det for det in frame_targets.values()): - self.armband_counter += 1 - else: + # 针对袖标异常的处理:如果当前帧中所有人员均未检测到 "袖标" + armband_candidates = [] + if frame_targets: + if all("袖标" not in det for det in frame_targets.values()): + for person_id, detections in frame_targets.items(): + person_box = people_results[idx].get(person_id, {}).get('box') + armband_candidates.append({ + "person_id": person_id, + "person_box": person_box, + "detection": detections + }) + self.armband_counter += 1 + else: + self.armband_counter = 0 + # 触发各报警类别的聚合报警(一次报警记录、一次报警图片) + for category, candidates in frame_candidates.items(): + if candidates: + self.trigger_alarm_aggregated(frame, category, candidates) + # 针对袖标异常,如果连续帧达到阈值,则触发一次报警 + if self.armband_counter >= self.frame_threshold and armband_candidates: + self.trigger_alarm_aggregated(frame, 'armband', []) self.armband_counter = 0 - if self.armband_counter >= self.frame_threshold: - self.trigger_alarm(frame, 'armband') - self.armband_counter = 0 - - # 清除计数器中已丢失的 person id - self.cleanup_counters(current_ids) def main(self, stream_loader): """ @@ -815,9 +911,8 @@ break - class Alarm(): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController=None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController=None): self.pool = [] self.device = device self.thread_id = thread_id @@ -930,14 +1025,14 @@ class IntranetLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) self.start_time = time.time() # 脚本启动时间戳 print(f'start time = {self.start_time}') - # self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, - # device_thread_id=thread_id) + self.stream_loader = OpenCVStreamLoad(camera_url=device.input_stream_url, camera_code=device.code, + device_thread_id=thread_id) self.executor = ThreadPoolExecutor(max_workers=10) self.loop = asyncio.get_running_loop() @@ -951,16 +1046,17 @@ self.anQuanMaoList = [] self.siHeyiList = [] self.siHeyiUmd = None # 上中下气体检测用的四合一设备 + self.harmful_gas_manager = HarmfulGasManager() health_device_codes, harmful_device_codes = get_group_device_list(device.code) for health_device_code in health_device_codes: self.anQuanMaoList.append(HelmetDataProcessor(health_device_code, self.alarm.alarm_record_center)) for harmful_device_code in harmful_device_codes: - self.siHeyiList.append(SiHeYi(harmful_device_code)) + self.siHeyiList.append(SiHeYi(harmful_device_code,self.harmful_gas_manager)) if self.siHeyiList: self.siHeyiUmd = self.siHeyiList[0] # todo 暂时先用第一个,后期要有标识标明用哪个 else: - self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98') + self.siHeyiUmd = SiHeYi('ZA0024587CC4CA98',self.harmful_gas_manager) async def laobaoCheck_task(self): # executor = ThreadPoolExecutor(max_workers=3) @@ -1010,7 +1106,7 @@ # executor = ThreadPoolExecutor(max_workers=3) # loop = asyncio.get_running_loop() await self.loop.run_in_executor(self.executor, self.yinhuan_check.main_fake, - r"D:\workspace\pythonProject\safe-algo-pro\2025-02-26 08-49-39.mkv") + r"D:\workspace\pythonProject\safe-algo-pro\1.mp4") async def xinlvCheck_task(self): @@ -1059,31 +1155,31 @@ logger.exception(f"任务 {task.get_name()} 发生异常: {e}") # 并行执行任务 - # uMDGasCheck_task = asyncio.create_task( - # self.uMDGasCheck_task(self.eventController)) - # laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) + uMDGasCheck_task = asyncio.create_task( + self.uMDGasCheck_task(self.eventController)) + laobaoCheck_task = asyncio.create_task(self.laobaoCheck_task()) alarm_task = asyncio.create_task(self.alarm_task()) # 给所有任务添加异常处理 - # for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: - # task.add_done_callback(handle_task_exception) - alarm_task.add_done_callback(handle_task_exception) + for task in [uMDGasCheck_task, laobaoCheck_task, alarm_task]: + task.add_done_callback(handle_task_exception) + # alarm_task.add_done_callback(handle_task_exception) - # done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=300000.0) - # - # if uMDGasCheck_task in done and laobaoCheck_task in done: - # await uMDGasCheck_task - # await laobaoCheck_task - # self.eventController.timeout_event.set() - # self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) - # print("前置条件检查完成,退出") - # - # else: - # # 如果超时,则取消未完成的任务 - # self.eventController.timeout_event.set() - # laobaoCheck_task.cancel() - # uMDGasCheck_task.cancel() - # print("前置条件检查时间过长,退出") + done, pending = await asyncio.wait({uMDGasCheck_task, laobaoCheck_task}, timeout=60*10) + + if uMDGasCheck_task in done and laobaoCheck_task in done: + await uMDGasCheck_task + await laobaoCheck_task + self.eventController.timeout_event.set() + self.alarm.alarm_message_center.send_immediate_command(PREPARE_COMPLETE_MESSAGE) + print("前置条件检查完成,退出") + + else: + # 如果超时,则取消未完成的任务 + self.eventController.timeout_event.set() + laobaoCheck_task.cancel() + uMDGasCheck_task.cancel() + print("前置条件检查时间过长,退出") # 删除前面产生的报警 self.alarm.deleteAlarmOfLaoBao() diff --git a/scene_handler/limit_space_scene_handler.py b/scene_handler/limit_space_scene_handler.py index a7f1674..5eb9dab 100644 --- a/scene_handler/limit_space_scene_handler.py +++ b/scene_handler/limit_space_scene_handler.py @@ -20,7 +20,7 @@ from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager COLOR_RED = (0, 0, 255) COLOR_GREEN = (255, 0, 0) @@ -145,7 +145,7 @@ class LimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) # self.device = device # self.thread_id = thread_id diff --git a/scene_handler/zyn_limit_space_scene_handler.py b/scene_handler/zyn_limit_space_scene_handler.py index e300098..0463020 100644 --- a/scene_handler/zyn_limit_space_scene_handler.py +++ b/scene_handler/zyn_limit_space_scene_handler.py @@ -24,7 +24,7 @@ from scene_handler.alarm_message_center import AlarmMessageCenter from scene_handler.base_scene_handler import BaseSceneHandler from services.global_config import GlobalConfig -from tcp.tcp_manager import TcpManager +from tcp.tcp_client_manager import TcpClientManager last_time = "" def create_value_iterator(values): @@ -623,7 +623,7 @@ self.isAlarm() class Alarm(): - def __init__(self,device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, eventController = None): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, eventController = None): self.pool = [] self.device = device self.thread_id = thread_id @@ -689,7 +689,7 @@ class ZynLimitSpaceSceneHandler(BaseSceneHandler): - def __init__(self, device: Device, thread_id: str, tcp_manager: TcpManager, main_loop, range_points): + def __init__(self, device: Device, thread_id: str, tcp_manager: TcpClientManager, main_loop, range_points): super().__init__(device=device, thread_id=thread_id, tcp_manager=tcp_manager, main_loop=main_loop) diff --git a/tcp/gas_device_handler.py b/tcp/gas_device_handler.py new file mode 100644 index 0000000..b9c5f6e --- /dev/null +++ b/tcp/gas_device_handler.py @@ -0,0 +1,111 @@ +import copy +import json +import asyncio +from datetime import datetime + +from common.byte_utils import format_bytes +from common.global_logger import logger +from common.http_utils import send_request_async +from db.database import get_db +from entity.data_gas import DataGas +from scene_handler.alarm_record_center import AlarmRecordCenter +from services.data_gas_service import DataGasService +from services.global_config import GlobalConfig + + +def parse_gas_data(data): + # 数据长度检查,确保最小长度符合协议要求 + if len(data) < 13: + return None + # raise ValueError("数据长度不足,无法解析") + + # 检查帧头(AA 01) + if data[6:8] != b'\xAA\x01': + return None + # raise ValueError("帧头不匹配") + + + # 解析设备编号(UU VV WW XX YY ZZ) + device_id = ''.join(f'{byte:02X}' for byte in data[:6]) + + + + # 解析 GG, HH, II 字节,计算燃气浓度值 (ppm.m) + GG = data[8] + HH = data[9] + II = data[10] + gas_concentration = GG * 65536 + HH * 256 + II + if gas_concentration > 99999: + raise ValueError("燃气浓度值超出范围") + + # 解析激光光强等级 JJ + JJ = data[11] + if not (0 <= JJ <= 14): + raise ValueError("激光光强等级超出范围") + + # 校验和 SU 验证,从第8到第12字节累加 + SU_received = data[12] + SU_calculated = sum(data[7:12]) & 0xFF # 取累加值的低8位 + if SU_received != SU_calculated: + raise ValueError(f"校验和不匹配: 预期 {SU_calculated:02X}, 实际 {SU_received:02X}") + + # 返回解析的结果 + return { + "device_code": device_id, + "gas_value": int(gas_concentration), + "laser_intensity_level": JJ, + "checksum_valid": SU_received == SU_calculated + } + + +# 业务处理层:解析气体数据、保存数据库、推送数据 +class GasDataHandler: + + alarm_dict = { + 'alarmType': '1', + 'alarmContent': '甲烷浓度超限', + } + + def __init__(self, main_loop=None): + self.last_push = {} + self.alarm_record_center = AlarmRecordCenter(main_loop=main_loop) + + async def handle_data(self, data: bytes): + try: + res = parse_gas_data(data) + if res: + logger.info(f"解析甲烷数据:{format_bytes(data)} {res}") + async for db in get_db(): + data_gas_service = DataGasService(db) + data_gas = DataGas( + device_code=res['device_code'], + gas_value=res['gas_value'] + ) + await data_gas_service.add_data_gas(data_gas) + self.push_data(data_gas) + self.handle_alarm(data_gas) + except Exception as e: + logger.exception(f"处理甲烷数据出错: {e}") + + def push_data(self, data_gas): + global_config = GlobalConfig() + gas_push_config = global_config.get_gas_push_config() + if gas_push_config and gas_push_config.push_url and gas_push_config.push_interval > 0: + last_ts = self.last_push.get(data_gas.device_code) + current_time = datetime.now() + + # 检查是否需要推送数据 + if last_ts is None or (current_time - last_ts).total_seconds() > gas_push_config.push_interval: + send_data = json.loads(copy.deepcopy(data_gas.json())) + send_data.pop("id") + asyncio.create_task(send_request_async(gas_push_config.push_url, send_data)) + self.last_push[data_gas.device_code] = current_time # 更新推送时间戳 + + def handle_alarm(self, data_gas): + if data_gas.gas_value > 100: + logger.info(f"甲烷浓度超限报警: {data_gas}") + self.alarm_record_center.upload_alarm_record(device_code=data_gas.device_code, + alarm_dict=self.alarm_dict, + alarm_value=data_gas.gas_value) + else: + logger.info(f"甲烷浓度正常: {data_gas}") \ No newline at end of file diff --git a/tcp/harmful_device_handler.py b/tcp/harmful_device_handler.py index ecbdd9c..dd48070 100644 --- a/tcp/harmful_device_handler.py +++ b/tcp/harmful_device_handler.py @@ -101,10 +101,10 @@ # 判断异常并上报 if gas_type is not None and gas_data is not None: if self._is_data_alarm(device_code, gas_type, gas_data): - print(f"报警: {device_code}, {gas_type}, {gas_data}") + print(f"四合一浓度报警: {device_code}, {gas_type}, {gas_data}") self._save_and_send_alarm(device_code, gas_type, gas_data) - print(self._harmful_gas_manager.get_device_all_data(device_code)) + print(f'更新四合一{device_code}数据 {self._harmful_gas_manager.get_device_all_data(device_code)}') except json.JSONDecodeError: logger.error(f"JSON解析错误: {message}") @@ -197,10 +197,7 @@ encoded_bytes = base64.b64encode(message.encode('utf-8')) # 将字节编码结果转换为字符串 encoded_string = encoded_bytes.decode('utf-8') - logger.debug(f'before encode: {message}') - logger.debug(f'after encode: {encoded_string}') push_message = {"content": encoded_string} - logger.debug(f'body: {push_message}') asyncio.create_task(send_request_async(harmful_push_config.push_url, push_message)) self._push_ts_dict[device_code] = current_time # 更新推送时间戳 else: diff --git a/tcp/tcp_client_connector.py b/tcp/tcp_client_connector.py index 6e0b776..1fb81e9 100644 --- a/tcp/tcp_client_connector.py +++ b/tcp/tcp_client_connector.py @@ -13,7 +13,9 @@ from services.data_gas_service import DataGasService from services.global_config import GlobalConfig - +''' +这个文件目前没有用了 +''' def parse_gas_data(data): # 数据长度检查,确保最小长度符合协议要求 if len(data) < 13: diff --git a/tcp/tcp_client_manager.py b/tcp/tcp_client_manager.py new file mode 100644 index 0000000..74d9c8d --- /dev/null +++ b/tcp/tcp_client_manager.py @@ -0,0 +1,48 @@ +import asyncio +from typing import List, Dict + +from common.consts import DEVICE_TYPE, NotifyChangeType, TREE_COMMAND +from db.database import get_db +from entity.device import Device +from services.device_service import DeviceService + +from common.global_logger import logger +from tcp.gas_device_handler import GasDataHandler +from tcp.tcp_connection import TcpConnection + + +# TcpClientManager 负责管理各设备的 TcpConnection,并提供对外接口 +class TcpClientManager: + def __init__(self, device_service, main_loop=None): + self.main_loop = main_loop + self.device_service = device_service + self.connector_map = {} # device_id -> TcpConnection + + async def start(self): + """从数据库加载设备并连接所有设备""" + devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 + logger.info(f"get {len(devices)} tree devices") + for device in devices: + await self.start_device_connect(device) + + async def start_device_connect(self, device): + if device.id in self.connector_map: + logger.warning(f"设备 {device.id} 已连接") + return + connector = TcpConnection(ip=device.gas_ip, port=333) + asyncio.create_task(connector.connection_monitor()) + handler = GasDataHandler(main_loop=self.main_loop) + connector.register_data_handler(handler.handle_data) + self.connector_map[device.id] = connector + asyncio.create_task(connector.connect()) + # 启动定时查询任务(查询间隔由设备配置决定) + asyncio.create_task( + connector.start_periodic_query(query_command=TREE_COMMAND.GAS_QUERY, interval=3)) + + async def send_message_to_device(self, device_id: int, message: bytes, have_response: bool = True): + if device_id not in self.connector_map: + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + connector = self.connector_map.get(device_id) + if connector: + await connector.send_message(message, have_response=have_response) diff --git a/tcp/tcp_client_manager.py.bak b/tcp/tcp_client_manager.py.bak new file mode 100644 index 0000000..22a0d89 --- /dev/null +++ b/tcp/tcp_client_manager.py.bak @@ -0,0 +1,89 @@ +import asyncio +from typing import List, Dict + + +from common.consts import DEVICE_TYPE, NotifyChangeType +from db.database import get_db +from entity.device import Device +from services.device_service import DeviceService + +from common.global_logger import logger +from services.global_config import GlobalConfig +from tcp.tcp_client_connector import TcpClientConnector + + +class TcpClientManager: + def __init__(self, device_service: DeviceService): + self.devices: List[Device] = [] + self.connector_map: Dict[int, TcpClientConnector] = {} + + self.device_service = device_service + + # 注册设备和模型的变化回调 + # self.device_service.register_change_callback(self.on_device_change) + + async def load_and_connect_devices(self): + """从数据库加载设备并连接所有设备""" + devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 + logger.info(f"get {len(devices)} tree devices") + for device in devices: + await self.start_device_connect(device) + + async def start(self): + """启动设备管理器""" + await self.load_and_connect_devices() + + async def start_device_connect(self, device: Device): + if device and int(device.type) == DEVICE_TYPE.TREE and device.gas_ip: + if device.id in self.connector_map: + logger.warning(f"Device {device.id} is already connected.") + return # 防止重复连接 + connector = TcpClientConnector(ip=device.gas_ip, port=333) + self.connector_map[device.id] = connector + asyncio.create_task(connector.connect()) + + async def stop_device_connect(self, device_id): + if device_id in self.connector_map: + connector = self.connector_map.pop(device_id) + await self.disconnect_device(connector) + + async def restart_device_thread(self, device_id): + await self.stop_device_connect(device_id) + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + + async def on_device_change(self, device_id, change_type): + """设备变化时的回调处理""" + if change_type == NotifyChangeType.DEVICE_CREATE: + # 新增设备,加载新设备并连接 + new_device = await self.device_service.get_device(device_id) + await self.start_device_connect(new_device) + + elif change_type == NotifyChangeType.DEVICE_DELETE: + await self.stop_device_connect(device_id) + + elif change_type == NotifyChangeType.DEVICE_UPDATE: + # 更新设备信息,重新连接 + await self.restart_device_thread(device_id) + + async def disconnect_device(self, connector: TcpClientConnector): + """断开设备连接""" + await connector.disconnect() + + async def send_message_to_device(self, device_id, message: bytes, have_response): + if device_id not in self.connector_map: + device = await self.device_service.get_device(device_id) + await self.start_device_connect(device) + connector = self.connector_map[device_id] + if connector: + await connector.send_message(message, have_response=have_response) + + + +# if __name__ == '__main__': + # async for db in get_db(): + # global_config = GlobalConfig() + # await global_config.init_config() + # device_service = DeviceService(db) + # tcp_manager = TcpManager(device_service) + # asyncio.run(tcp_manager.start()) diff --git a/tcp/tcp_connection.py b/tcp/tcp_connection.py new file mode 100644 index 0000000..1dd433d --- /dev/null +++ b/tcp/tcp_connection.py @@ -0,0 +1,148 @@ +import asyncio +from common.global_logger import logger + + +# TcpConnection 类只负责连接、重连、收发消息,同时维护一个消息队列 +class TcpConnection: + def __init__(self, ip: str, port: int, timeout: float = 5, reconnect_interval: float = 3): + self.ip = ip + self.port = port + self.timeout = timeout + self.reconnect_interval = reconnect_interval + self.reader = None + self.writer = None + self.is_connected = False + self.data_handler = None # 上层业务处理回调 async function(data: bytes) + self.send_lock = asyncio.Lock() + self.read_lock = asyncio.Lock() + self.message_queue = asyncio.Queue() # 存放待发送的消息元组 (message, have_response) + self.response_queue = asyncio.Queue() # 存放所有读取到的响应数据 + self._read_task = None + self._message_task = None + + async def connect(self): + while not self.is_connected: + try: + logger.info(f"正在连接 {self.ip}:{self.port}") + self.reader, self.writer = await asyncio.wait_for( + asyncio.open_connection(self.ip, self.port), + timeout=self.timeout + ) + + # 验证连接是否真正建立 + if self.writer is None or self.writer.is_closing(): + raise ConnectionError("连接未能成功建立") + + self.is_connected = True + logger.info(f"已连接 {self.ip}:{self.port}") + + # 取消可能遗留的任务,再启动新的后台任务 + if self._read_task is not None: + self._read_task.cancel() + if self._message_task is not None: + self._message_task.cancel() + self._read_task = asyncio.create_task(self._read_loop()) + self._message_task = asyncio.create_task(self._process_message_queue()) + except Exception as e: + logger.error(f"连接 {self.ip}:{self.port} 失败: {e},{self.reconnect_interval}s后重试") + await asyncio.sleep(self.reconnect_interval) + + async def connection_monitor(self): + """外部启动的连接监控任务,负责在连接断开时重连""" + while True: + if not self.is_connected: + await self.connect() + await asyncio.sleep(1) # 根据需要调整检测间隔 + + async def disconnect(self): + if self.writer: + self.writer.close() + try: + await self.writer.wait_closed() + except Exception as e: + logger.error(f"关闭连接异常: {e}") + self.reader = None + self.writer = None + self.is_connected = False + # 取消后台任务,防止它们继续访问已断开的连接 + if self._read_task: + self._read_task.cancel() + self._read_task = None + if self._message_task: + self._message_task.cancel() + self._message_task = None + logger.info(f"断开连接 {self.ip}:{self.port}") + + async def _read_loop(self): + while self.is_connected: + # 如果 self.reader 为 None,等待后再重试 + if self.reader is None: + await asyncio.sleep(0.1) + continue + try: + async with self.read_lock: + data = await asyncio.wait_for(self.reader.read(1024), timeout=self.timeout) + if data: + logger.info(f"从 {self.ip}:{self.port} 收到数据: {data}") + await self.response_queue.put(data) + if self.data_handler: + await self.data_handler(data) + # else: + # logger.warning("未收到数据,断开连接") + # await self.disconnect() + # await self.connect() + except Exception as e: + logger.exception(f"读取数据出错 {self.ip}:{self.port}: {e}") + await self.disconnect() + # await self.connect() + + async def _process_message_queue(self): + while self.is_connected: + message, have_response = await self.message_queue.get() + await self._send_message_with_retry(message, have_response) + + async def _send_message_with_retry(self, message: bytes, have_response: bool): + try: + async with self.send_lock: + if not self.is_connected: + await self.connect() + self.writer.write(message) + await self.writer.drain() + logger.info(f"向 {self.ip}:{self.port} 发送消息: {message}") + if have_response: + # 等待统一读取任务读取到响应 + response = await asyncio.wait_for(self.response_queue.get(), timeout=self.timeout) + return response + # if have_response and self.data_handler: + # async with self.read_lock: + # data = await asyncio.wait_for(self.reader.read(1024), timeout=self.timeout) + # await self.data_handler(data) + except Exception as e: + logger.exception(f"发送消息失败: {e}") + # 重新入队,等待重连后再次发送 + await self.message_queue.put((message, have_response)) + await self.disconnect() + # await self.connect() + + async def send_message(self, message: bytes, have_response: bool = True): + """将消息放入发送队列""" + await self.message_queue.put((message, have_response)) + logger.info(f"消息已加入队列: {message}") + + def register_data_handler(self, handler): + """注册处理接收数据的回调函数,handler 应为 async function(data: bytes)""" + self.data_handler = handler + + async def start_periodic_query(self, query_command: bytes, interval: float): + """自动定时发送查询指令,无论连接状态如何均持续执行""" + while True: + if not self.is_connected: + logger.info("当前未连接,等待重连...") + await asyncio.sleep(1) + continue + try: + # 将查询消息也放入队列,确保顺序一致 + await self.send_message(query_command, have_response=True) + except Exception as e: + logger.error(f"定时查询发送失败: {e}") + await asyncio.sleep(interval) diff --git a/tcp/tcp_manager.py b/tcp/tcp_manager.py deleted file mode 100644 index f7ed12e..0000000 --- a/tcp/tcp_manager.py +++ /dev/null @@ -1,89 +0,0 @@ -import asyncio -from typing import List, Dict - - -from common.consts import DEVICE_TYPE, NotifyChangeType -from db.database import get_db -from entity.device import Device -from services.device_service import DeviceService - -from common.global_logger import logger -from services.global_config import GlobalConfig -from tcp.tcp_client_connector import TcpClientConnector - - -class TcpManager: - def __init__(self, device_service: DeviceService): - self.devices: List[Device] = [] - self.connector_map: Dict[int, TcpClientConnector] = {} - - self.device_service = device_service - - # 注册设备和模型的变化回调 - # self.device_service.register_change_callback(self.on_device_change) - - async def load_and_connect_devices(self): - """从数据库加载设备并连接所有设备""" - devices = await self.device_service.get_device_list(device_type=DEVICE_TYPE.TREE) # 使用局部变量 - logger.info(f"get {len(devices)} tree devices") - for device in devices: - await self.start_device_connect(device) - - async def start(self): - """启动设备管理器""" - await self.load_and_connect_devices() - - async def start_device_connect(self, device: Device): - if device and int(device.type) == DEVICE_TYPE.TREE and device.gas_ip: - if device.id in self.connector_map: - logger.warning(f"Device {device.id} is already connected.") - return # 防止重复连接 - connector = TcpClientConnector(ip=device.gas_ip, port=333) - self.connector_map[device.id] = connector - asyncio.create_task(connector.connect()) - - async def stop_device_connect(self, device_id): - if device_id in self.connector_map: - connector = self.connector_map.pop(device_id) - await self.disconnect_device(connector) - - async def restart_device_thread(self, device_id): - await self.stop_device_connect(device_id) - device = await self.device_service.get_device(device_id) - await self.start_device_connect(device) - - async def on_device_change(self, device_id, change_type): - """设备变化时的回调处理""" - if change_type == NotifyChangeType.DEVICE_CREATE: - # 新增设备,加载新设备并连接 - new_device = await self.device_service.get_device(device_id) - await self.start_device_connect(new_device) - - elif change_type == NotifyChangeType.DEVICE_DELETE: - await self.stop_device_connect(device_id) - - elif change_type == NotifyChangeType.DEVICE_UPDATE: - # 更新设备信息,重新连接 - await self.restart_device_thread(device_id) - - async def disconnect_device(self, connector: TcpClientConnector): - """断开设备连接""" - await connector.disconnect() - - async def send_message_to_device(self, device_id, message: bytes, have_response): - if device_id not in self.connector_map: - device = await self.device_service.get_device(device_id) - await self.start_device_connect(device) - connector = self.connector_map[device_id] - if connector: - await connector.send_message(message, have_response=have_response) - - - -# if __name__ == '__main__': - # async for db in get_db(): - # global_config = GlobalConfig() - # await global_config.init_config() - # device_service = DeviceService(db) - # tcp_manager = TcpManager(device_service) - # asyncio.run(tcp_manager.start())