diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/lib/libavfilter.so.7 b/lib/libavfilter.so.7 new file mode 100644 index 0000000..a9a2d0b --- /dev/null +++ b/lib/libavfilter.so.7 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/lib/libavfilter.so.7 b/lib/libavfilter.so.7 new file mode 100644 index 0000000..a9a2d0b --- /dev/null +++ b/lib/libavfilter.so.7 Binary files differ diff --git a/lib/libavformat.so.58 b/lib/libavformat.so.58 new file mode 100644 index 0000000..c230921 --- /dev/null +++ b/lib/libavformat.so.58 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/lib/libavfilter.so.7 b/lib/libavfilter.so.7 new file mode 100644 index 0000000..a9a2d0b --- /dev/null +++ b/lib/libavfilter.so.7 Binary files differ diff --git a/lib/libavformat.so.58 b/lib/libavformat.so.58 new file mode 100644 index 0000000..c230921 --- /dev/null +++ b/lib/libavformat.so.58 Binary files differ diff --git a/lib/libavutil.so.56 b/lib/libavutil.so.56 new file mode 100644 index 0000000..d9581ff --- /dev/null +++ b/lib/libavutil.so.56 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/lib/libavfilter.so.7 b/lib/libavfilter.so.7 new file mode 100644 index 0000000..a9a2d0b --- /dev/null +++ b/lib/libavfilter.so.7 Binary files differ diff --git a/lib/libavformat.so.58 b/lib/libavformat.so.58 new file mode 100644 index 0000000..c230921 --- /dev/null +++ b/lib/libavformat.so.58 Binary files differ diff --git a/lib/libavutil.so.56 b/lib/libavutil.so.56 new file mode 100644 index 0000000..d9581ff --- /dev/null +++ b/lib/libavutil.so.56 Binary files differ diff --git a/lib/libmp3lame.so.0 b/lib/libmp3lame.so.0 new file mode 100644 index 0000000..dbea058 --- /dev/null +++ b/lib/libmp3lame.so.0 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/lib/libavfilter.so.7 b/lib/libavfilter.so.7 new file mode 100644 index 0000000..a9a2d0b --- /dev/null +++ b/lib/libavfilter.so.7 Binary files differ diff --git a/lib/libavformat.so.58 b/lib/libavformat.so.58 new file mode 100644 index 0000000..c230921 --- /dev/null +++ b/lib/libavformat.so.58 Binary files differ diff --git a/lib/libavutil.so.56 b/lib/libavutil.so.56 new file mode 100644 index 0000000..d9581ff --- /dev/null +++ b/lib/libavutil.so.56 Binary files differ diff --git a/lib/libmp3lame.so.0 b/lib/libmp3lame.so.0 new file mode 100644 index 0000000..dbea058 --- /dev/null +++ b/lib/libmp3lame.so.0 Binary files differ diff --git a/lib/libswresample.so.3 b/lib/libswresample.so.3 new file mode 100644 index 0000000..5ab957b --- /dev/null +++ b/lib/libswresample.so.3 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/lib/libavfilter.so.7 b/lib/libavfilter.so.7 new file mode 100644 index 0000000..a9a2d0b --- /dev/null +++ b/lib/libavfilter.so.7 Binary files differ diff --git a/lib/libavformat.so.58 b/lib/libavformat.so.58 new file mode 100644 index 0000000..c230921 --- /dev/null +++ b/lib/libavformat.so.58 Binary files differ diff --git a/lib/libavutil.so.56 b/lib/libavutil.so.56 new file mode 100644 index 0000000..d9581ff --- /dev/null +++ b/lib/libavutil.so.56 Binary files differ diff --git a/lib/libmp3lame.so.0 b/lib/libmp3lame.so.0 new file mode 100644 index 0000000..dbea058 --- /dev/null +++ b/lib/libmp3lame.so.0 Binary files differ diff --git a/lib/libswresample.so.3 b/lib/libswresample.so.3 new file mode 100644 index 0000000..5ab957b --- /dev/null +++ b/lib/libswresample.so.3 Binary files differ diff --git a/lib/libswscale.so.5 b/lib/libswscale.so.5 new file mode 100644 index 0000000..f573cbd --- /dev/null +++ b/lib/libswscale.so.5 Binary files differ diff --git a/README.md b/README.md index a70c964..a349fe7 100644 --- a/README.md +++ b/README.md @@ -48,5 +48,7 @@ 点位3:识别 调压箱(目前没识别),???,报【XXXX,燃气浓度正常/异常】 以上都是机器狗走到目标点位,才执行 +甲烷浓度异常时,语音报警 -另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) \ No newline at end of file +另外全程实时检测第三方施工,检测到目标物,报【识别到XX(目标名称),发现第三方施工】:保证识别目标:路锥、工服(反光衣) +同时语音报警 \ No newline at end of file diff --git a/audio_files/1.mp3 b/audio_files/1.mp3 new file mode 100644 index 0000000..7b278fc --- /dev/null +++ b/audio_files/1.mp3 Binary files differ diff --git a/audio_files/2.mp3 b/audio_files/2.mp3 new file mode 100644 index 0000000..bb403cf --- /dev/null +++ b/audio_files/2.mp3 Binary files differ diff --git a/audio_player.py b/audio_player.py new file mode 100644 index 0000000..88987fb --- /dev/null +++ b/audio_player.py @@ -0,0 +1,152 @@ +import ctypes +import os +import threading +import time + +from ctypes import * +from config import AUDIO_DEVICE_IP, AUDIO_CLIENT_IP, GAS_AUDIO_FILE, CONSTRUCTION_AUDIO_FILE + +from global_logger import logger + + +# 错误类 +class CustomError(Exception): + def __init__(self, ErrorInfo): + super().__init__(self) + self.errorinfo = ErrorInfo + + def __str__(self): + return self.errorinfo + + +# 播放信息结构体 +class PlayInfo(Structure): + _fields_ = [ + ("playstatus", c_int), + ("progress", c_int), + ("totaltime", c_int), + ("samplerate", c_int), + ("bitrate", c_int) + ] + + +# 底层接口封装类 +class NAudioClient: + def __init__(self, local_ip, device_ip): + self.local_ip = local_ip + self.device_ip = device_ip + + self.dll = cdll.LoadLibrary("./lib/libNAudioClient.so") + + self._init_client() + + def _init_client(self): + # 回调函数定义 + def PlayStatusChangeCallback(scene, inst, status): + sceneList = ["文件播放", "TTS播放", "实时寻呼", "音频流播放"] + statusList = ["停止", "播放中..", "暂停中.."] + print(f"播放状态改变,场景: {sceneList[scene]}, 实例: {inst + 1}, 状态: {statusList[status]}") + return 0 + + self.PSCCB = CFUNCTYPE(c_int, c_byte, c_byte, c_uint)(PlayStatusChangeCallback) + # 初始化 + char_p_localip = bytes(self.local_ip, "utf8") + self.dll.na_client_init.restype = c_int + self.dll.na_client_init.argtypes = [c_char_p] + res = self.dll.na_client_init(char_p_localip) + if res != 0: + raise CustomError('通信设备初始化失败!') + # 启动 + self.dll.na_set_callback.restype = None + self.dll.na_client_start.restype = c_int + self.dll.na_client_start.argtypes = [CFUNCTYPE(c_int, c_byte, c_byte, c_uint)] + res = self.dll.na_client_start(self.PSCCB) + if res != 0: + raise CustomError('启动客户端失败!') + + def set_play_volume(self, volume=80): + self.dll.na_set_play_volume.restype = c_int + self.dll.na_set_play_volume.argtypes = [c_byte, c_byte, c_int] + res = self.dll.na_set_play_volume(0, 0, volume) + if res != 0: + raise CustomError('设置音量失败!') + + def clear_play_device(self): + self.dll.na_clear_play_device.restype = c_int + self.dll.na_clear_play_device.argtypes = [c_byte, c_byte] + res = self.dll.na_clear_play_device(0, 0) + if res != 0: + raise CustomError('清空播放设备失败!') + + def add_play_device(self): + self.dll.na_add_play_device.restype = c_int + self.dll.na_add_play_device.argtypes = [c_byte, c_byte, c_char_p] + char_p_devip = bytes(self.device_ip, "utf8") + res = self.dll.na_add_play_device(0, 0, char_p_devip) + if res != 0: + raise CustomError('添加播放设备失败!') + + def start_play(self, filepath, timeout=10): + self.dll.na_start_play_1.restype = c_int + self.dll.na_start_play_1.argtypes = [c_byte, c_byte, c_char_p, c_int] + char_p_filepath = bytes(filepath, "utf8") + res = self.dll.na_start_play_1(0, 0, char_p_filepath, timeout) + if res != 0: + raise CustomError('启动播放失败!') + + def get_playinfo(self): + playinfo = PlayInfo() + self.dll.na_get_playinfo.restype = c_int + self.dll.na_get_playinfo.argtypes = [c_byte, c_byte, POINTER(PlayInfo)] + self.dll.na_get_playinfo(0, 0, byref(playinfo)) + return playinfo + + def stop(self): + self.dll.na_client_stop() + + +# 单例音频播放器 +class AudioPlayer: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + if not cls._instance: + with cls._lock: + if not cls._instance: + cls._instance = super().__new__(cls) + cls._instance._init_config() + cls._instance._init_client() + return cls._instance + + def _init_config(self): + self.device_ip = AUDIO_DEVICE_IP + self.local_ip = AUDIO_CLIENT_IP + + def _init_client(self): + logger.info(f"初始化音频播放设备,设备IP: {self.device_ip}, 客户端IP: {self.local_ip}") + self.client = NAudioClient(self.local_ip, self.device_ip) + + def play_audio_file(self, filepath): + logger.info(f"开始播放音频文件: {filepath}") + self.client.set_play_volume(80) + self.client.clear_play_device() + self.client.add_play_device() + self.client.start_play(filepath, 10) + while True: + playinfo = self.client.get_playinfo() + if playinfo.playstatus == 0: + break + time.sleep(1) + + def stop(self): + self.client.stop() + + +if __name__ == "__main__": + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) +# 用法示例: +# player = AudioPlayer('config.ini') +# player.play_audio_file('/path/to/audio.mp3') diff --git a/camera_processor.py b/camera_processor.py index 4f8c128..689eedf 100644 --- a/camera_processor.py +++ b/camera_processor.py @@ -9,7 +9,8 @@ import json import os -from config import BIZ_CLASS +from audio_player import AudioPlayer +from config import BIZ_CLASS, CONSTRUCTION_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, colors, COLOR_RED @@ -28,7 +29,6 @@ self.cam_id = self.camera_config.get('cam_id') self.gst_str = self.camera_config.get('gst_str') self.tcp_send_cls = self.camera_config.get('tcp_send_cls', []) - self.alarm_send_cls = self.camera_config.get('alarm_send_cls', []) self.biz_cls = sum(BIZ_CLASS.values(), []) # 摄像头状态 @@ -79,8 +79,8 @@ self.cap.release() # 确保释放之前的资源 logger.info(f"摄像头 {self.cam_id} 正在打开,源: {self.gst_str}") - self.cap = cv2.VideoCapture(self.gst_str) - # self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) + # self.cap = cv2.VideoCapture(self.gst_str) + self.cap = cv2.VideoCapture(self.gst_str, cv2.CAP_GSTREAMER) if not self.cap.isOpened(): logger.error(f"摄像头 {self.cam_id} 打开失败") @@ -184,6 +184,8 @@ # 上传第三方施工报警 if any(label in BIZ_CLASS['CONSTRUCTION'] for label in frame_labels): + player = AudioPlayer() + player.play_audio_file(CONSTRUCTION_AUDIO_FILE) if self.last_alarm_ts is not None and time.time() - self.last_alarm_ts < self.alarm_send_interval: continue diff --git a/config.py b/config.py index 0eed53b..126064e 100644 --- a/config.py +++ b/config.py @@ -1,28 +1,27 @@ # config.py CAMERAS = [ - # { - # "cam_id": 0, - # "gst_str": ( - # "v4l2src device=/dev/video0 ! " - # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # "jpegdec ! videoconvert ! appsink" - # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! " - # # "image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! appsink" - # # ), - # # "gst_str": ( - # # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " - # # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" - # # ), - # "tcp_send_cls": ["井盖眼"], - # "alarm_send_cls": ["路锥", "反光衣"], - # "remark": "机械臂摄像头", - # "save_annotated_images": True, - # "frame_interval": 5, - # "receive_capture_command": True - # }, + { + "cam_id": 0, + "gst_str": ( + "v4l2src device=/dev/video0 ! " + "image/jpeg, width=1280, height=720, framerate=30/1 ! " + "jpegdec ! videoconvert ! appsink" + ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! " + # "image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! appsink" + # ), + # "gst_str": ( + # "v4l2src device=/dev/video0 ! image/jpeg, width=1280, height=720, framerate=30/1 ! " + # "jpegparse ! jpegdec ! videoconvert ! video/x-raw, format=BGR ! appsink drop=true sync=false" + # ), + "tcp_send_cls": ["井盖眼"], + "remark": "机械臂摄像头", + "save_annotated_images": True, + "frame_interval": 5, + "receive_capture_command": True + }, # { # "cam_id": 1, # "gst_str": ( @@ -36,20 +35,20 @@ # "save_annotated_images": True, # "frame_interval": 5, # }, - { - "cam_id": 0, - "gst_str": 0, - "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? - "alarm_send_cls": ["路锥", "反光衣", "围挡"], - "frame_interval": 5, - "remark": "本地测试摄像头", - "save_annotated_images": True, - "receive_capture_command": True - }, + # { + # "cam_id": 0, + # "gst_str": 0, + # "tcp_send_cls": ["井盖眼"], # todo 要加引入口吗? + # "frame_interval": 5, + # "remark": "本地测试摄像头", + # "save_annotated_images": True, + # "receive_capture_command": True + # }, ] TCP_SERVER = { - "host": "127.0.0.1", + # "host": "127.0.0.1", + "host": "192.168.123.219", "port": 8888 } @@ -87,7 +86,7 @@ } MODEL = { - "path": "weights/go-v8s-20250117.pt", + "path": "weights/go-v8s-20250630.pt", "size": 640, "class_map": MODEL_CLASS, "batch_size": 1, @@ -107,4 +106,10 @@ "3": ('10008', '引入口监测', None), "4": ('10009', '调压箱监测 ', None), "5": ('10011', '燃气泄漏', None), # 使用None表示需要根据gas_data计算 -} \ No newline at end of file +} + +AUDIO_DEVICE_IP = "192.168.123.242" +AUDIO_CLIENT_IP = "192.168.123.18" + +GAS_AUDIO_FILE = 'audio_files/1.mp3' +CONSTRUCTION_AUDIO_FILE = 'audio_files/2.mp3' diff --git a/handle_tcp_command.py b/handle_tcp_command.py index 3964d20..d9af939 100644 --- a/handle_tcp_command.py +++ b/handle_tcp_command.py @@ -7,7 +7,8 @@ import cv2 -from config import BIZ_CLASS, PRESET_CONTENT_MAP +from audio_player import AudioPlayer +from config import BIZ_CLASS, PRESET_CONTENT_MAP, GAS_AUDIO_FILE from global_logger import logger, process_log_data from image_plotting import Annotator, COLOR_RED, COLOR_BLUE @@ -74,6 +75,10 @@ skip_img = int(preset) == 5 and float(gas_data) <= 0 gas_alarm = 0 if float(gas_data) <= 0 else 1 + if gas_alarm: + player = AudioPlayer() + player.play_audio_file(GAS_AUDIO_FILE) + # 获取基础配置 report_type, content, status = self.get_cap_content(preset, gas_data) jpg_data = None diff --git a/http_client.py b/http_client.py index 39e9e11..3acb840 100644 --- a/http_client.py +++ b/http_client.py @@ -18,7 +18,7 @@ async with session.post(self.url, json=data, timeout=self.timeout) as response: logger.info(f"HTTP 响应: {await response.text()}") except Exception as e: - logger.exception(f"HTTP 发送失败: {e}, 数据: {data}") + logger.exception(f"HTTP 发送失败: {e}") async def send(self, data: dict): await self.queue.put(data) diff --git a/lib/libNAudioClient.so b/lib/libNAudioClient.so new file mode 100644 index 0000000..0f2854b --- /dev/null +++ b/lib/libNAudioClient.so Binary files differ diff --git a/lib/libavcodec.so.58 b/lib/libavcodec.so.58 new file mode 100644 index 0000000..8757778 --- /dev/null +++ b/lib/libavcodec.so.58 Binary files differ diff --git a/lib/libavdevice.so.58 b/lib/libavdevice.so.58 new file mode 100644 index 0000000..40e2369 --- /dev/null +++ b/lib/libavdevice.so.58 Binary files differ diff --git a/lib/libavfilter.so.7 b/lib/libavfilter.so.7 new file mode 100644 index 0000000..a9a2d0b --- /dev/null +++ b/lib/libavfilter.so.7 Binary files differ diff --git a/lib/libavformat.so.58 b/lib/libavformat.so.58 new file mode 100644 index 0000000..c230921 --- /dev/null +++ b/lib/libavformat.so.58 Binary files differ diff --git a/lib/libavutil.so.56 b/lib/libavutil.so.56 new file mode 100644 index 0000000..d9581ff --- /dev/null +++ b/lib/libavutil.so.56 Binary files differ diff --git a/lib/libmp3lame.so.0 b/lib/libmp3lame.so.0 new file mode 100644 index 0000000..dbea058 --- /dev/null +++ b/lib/libmp3lame.so.0 Binary files differ diff --git a/lib/libswresample.so.3 b/lib/libswresample.so.3 new file mode 100644 index 0000000..5ab957b --- /dev/null +++ b/lib/libswresample.so.3 Binary files differ diff --git a/lib/libswscale.so.5 b/lib/libswscale.so.5 new file mode 100644 index 0000000..f573cbd --- /dev/null +++ b/lib/libswscale.so.5 Binary files differ diff --git a/weights/go-v8s-20250630.pt b/weights/go-v8s-20250630.pt new file mode 100644 index 0000000..687b9e4 --- /dev/null +++ b/weights/go-v8s-20250630.pt Binary files differ