diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so new file mode 100644 index 0000000..e066361 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so new file mode 100644 index 0000000..e066361 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 new file mode 100644 index 0000000..fa9aa0f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 Binary files differ diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so new file mode 100644 index 0000000..e066361 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 new file mode 100644 index 0000000..fa9aa0f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 Binary files differ diff --git a/rsa_utils.py b/rsa_utils.py new file mode 100644 index 0000000..a25fa2f --- /dev/null +++ b/rsa_utils.py @@ -0,0 +1,49 @@ +import base64 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.backends import default_backend + + +def encrypt_message_with_public_key(public_key_pem: str, message: str) -> str: + """ + 用公钥(字符串形式)对明文进行加密,并返回 base64 编码后的密文字符串 + """ + # 1. 从公钥字符串还原为公钥对象 + public_key = serialization.load_pem_public_key( + public_key_pem.encode("utf-8"), # 字符串转 bytes + backend=default_backend() + ) + + # 2. 对明文进行加密(OAEP 填充 + SHA256),返回二进制密文 + ciphertext_bytes = public_key.encrypt( + message.encode("utf-8"), + padding.PKCS1v15() + ) + + # 3. 将二进制密文转成 base64 字符串,方便通过 HTTP 或其他渠道传输 + ciphertext_base64 = base64.b64encode(ciphertext_bytes).decode("utf-8") + + return ciphertext_base64 + + +def decrypt_message_with_private_key(private_key_pem: str, ciphertext_base64: str) -> str: + """ + 用私钥(字符串形式)对 base64格式的密文进行解密,返回明文字符串 + """ + # 1. 从私钥字符串还原为私钥对象 + private_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), # 字符串转 bytes + password=None, # 如果在生成私钥时使用了加密算法,这里需要提供密码 + backend=default_backend() + ) + + # 2. Base64解码出二进制密文 + ciphertext_bytes = base64.b64decode(ciphertext_base64) + + # 3. 使用私钥解密 + plaintext_bytes = private_key.decrypt( + ciphertext_bytes, + padding.PKCS1v15() + ) + + return plaintext_bytes.decode("utf-8") \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so new file mode 100644 index 0000000..e066361 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 new file mode 100644 index 0000000..fa9aa0f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 Binary files differ diff --git a/rsa_utils.py b/rsa_utils.py new file mode 100644 index 0000000..a25fa2f --- /dev/null +++ b/rsa_utils.py @@ -0,0 +1,49 @@ +import base64 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.backends import default_backend + + +def encrypt_message_with_public_key(public_key_pem: str, message: str) -> str: + """ + 用公钥(字符串形式)对明文进行加密,并返回 base64 编码后的密文字符串 + """ + # 1. 从公钥字符串还原为公钥对象 + public_key = serialization.load_pem_public_key( + public_key_pem.encode("utf-8"), # 字符串转 bytes + backend=default_backend() + ) + + # 2. 对明文进行加密(OAEP 填充 + SHA256),返回二进制密文 + ciphertext_bytes = public_key.encrypt( + message.encode("utf-8"), + padding.PKCS1v15() + ) + + # 3. 将二进制密文转成 base64 字符串,方便通过 HTTP 或其他渠道传输 + ciphertext_base64 = base64.b64encode(ciphertext_bytes).decode("utf-8") + + return ciphertext_base64 + + +def decrypt_message_with_private_key(private_key_pem: str, ciphertext_base64: str) -> str: + """ + 用私钥(字符串形式)对 base64格式的密文进行解密,返回明文字符串 + """ + # 1. 从私钥字符串还原为私钥对象 + private_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), # 字符串转 bytes + password=None, # 如果在生成私钥时使用了加密算法,这里需要提供密码 + backend=default_backend() + ) + + # 2. Base64解码出二进制密文 + ciphertext_bytes = base64.b64decode(ciphertext_base64) + + # 3. 使用私钥解密 + plaintext_bytes = private_key.decrypt( + ciphertext_bytes, + padding.PKCS1v15() + ) + + return plaintext_bytes.decode("utf-8") \ No newline at end of file diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py new file mode 100644 index 0000000..83fc593 --- /dev/null +++ b/scene_handler/base_scene_handler.py @@ -0,0 +1,722 @@ +import struct +import numpy as np +import pylynchipsdk as sdk +import copy +import sys, os +import ctypes +from ctypes import * +import subprocess +import threading +import multiprocessing +import time + +from global_logger import logger +from blockqueue import block_queue +from callback_data_struct import * +import common +import bufferpool +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from info_query import ModelObject + +def save_file_cb(params): + cb_data: save_file_cb_data = params[0] + frame_cb_data: cb_data = params[1] + + packet = cb_data.packet + data_size, ret = sdk.lyn_enc_get_remote_packet_valid_size(packet) + common.error_check(ret, "lyn_enc_get_remote_packet_valid_size") + data = np.zeros(data_size, dtype=np.byte) + data_ptr = sdk.lyn_numpy_to_ptr(data) + ret = sdk.lyn_memcpy( + data_ptr, packet.data, data_size, sdk.lyn_memcpy_dir_t.ServerToClient + ) + common.error_check(ret, "lyn_memcpy") + + cb_data.video_frame.put([data, packet.eos]) + cb_data.recv_pool.push(packet.data) + return ret + +class SceneModelHandler: + def __init__(self, model_path, plugin_path, model_name,model_code, objects, image_width, image_height) -> None: + self.model_path = model_path + self.plugin_path = plugin_path + self.model_name = model_name + self.model_code = model_code + self.objects = objects + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + + self.model, ret = sdk.lyn_load_model(self.model_path) + common.error_check(ret, "lyn_load_model") + + self.plugin, ret = sdk.lyn_plugin_register(self.plugin_path) + common.error_check(ret, "lyn_plugin_register") + + self.apu_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.plugin_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.apu_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.model_desc, ret = sdk.lyn_model_get_desc(self.model) + common.error_check(ret, "lyn_model_get_desc") + self.batch_size = self.model_desc.inputTensorAttrArray[0].batchSize + self.model_width = self.model_desc.inputTensorAttrArray[0].dims[2] + self.model_height = self.model_desc.inputTensorAttrArray[0].dims[1] + + self.apu_output_size = self.model_desc.outputDataLen + + self.class_num = self.model_desc.outputTensorAttrArray[0].dims[3] - 5 + self.anchor_size = self.model_desc.outputTensorAttrArray[0].dims[1] + self.boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + + self.sub_objects_info, ret = sdk.lyn_malloc(ctypes.sizeof(SubObject)) + + sub_obj = sdk.c_malloc(ctypes.sizeof(SubObject)) + + # 获取底层指针 + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + pythonapi.PyCapsule_GetPointer.restype = c_void_p + raw_ptr = ctypes.pythonapi.PyCapsule_GetPointer(sub_obj, None) + if not raw_ptr: + raise ValueError("Failed to extract pointer from PyCapsule") + + # 转换为 ctypes.POINTER(SubObject) + sub_obj_ptr = ctypes.cast(raw_ptr, ctypes.POINTER(SubObject)) + + # 通过 ctypes.POINTER 操作数据 + sub_obj_instance = sub_obj_ptr.contents + sub_obj_instance.objectsnum = len(self.objects) + + for i in range(sub_obj_instance.objectsnum): + obj = self.objects[i] + sub_obj_instance.objects[i].object_id = int(obj.object_code) + sub_obj_instance.objects[i].object_name = obj.object_name + sub_obj_instance.objects[i].conf_threshold = obj.conf_threshold + sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold + sub_obj_instance.objects[i].model_code = obj.model_code + if obj.range: + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + abs_range = [] + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') + sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) + else: + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) + + ret = sdk.lyn_memcpy( + self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer + ) + common.error_check(ret, "save_boxinfo_cb lyn_memcpy") + + self.apu_output_mem_pool = bufferpool.buffer_pool( + self.apu_output_size * self.batch_size, 5 + ) + + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') + +class BaseSceneHandler: + def __init__(self, attr: device_process_attr) -> None: + self.attr = "" + self.__ctx = "" + self.__send_thread = "" + self.__recv_thread = "" + self.__ipe_thread = "" + self.__demux_hdl = "" + self.codec_para = "" + self.__vdec_hdl = "" + self.__vdec_attr = sdk.lyn_vdec_attr_t() + self.__send_queue = block_queue() + self.__ipe_queue = block_queue() + self.__send_num = 0 + self.__recv_num = 0 + # 创建上下文环境 + self.attr = copy.copy(attr) + self.video_frame = attr.video_frame + self.__ctx, ret = sdk.lyn_create_context(self.attr.device_id) + self.enc_head_flag = True + common.error_check(ret, "create context") + ret = sdk.lyn_register_error_handler(common.default_stream_error_handle) + common.error_check(ret, 'lyn_register_error_handler') + + # 打开解封装器 + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + common.error_check(ret, "lyn_demux_open ") + self.codec_para, ret = sdk.lyn_demux_get_codec_para(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_codec_para ") + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_framerate") + logger.debug(f'fps = {self.fps}') + + # 打开解码器 + self.__vdec_attr.codec_id = self.codec_para.codec_id + self.__vdec_attr.output_fmt = self.attr.output_fmt + self.__vdec_attr.scale = self.attr.scale + self.__vdec_hdl, ret = sdk.lyn_vdec_open(self.__vdec_attr) + common.error_check(ret, "lyn_vdec_open ") + self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( + self.codec_para, self.__vdec_attr + ) + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + common.error_check(ret, "lyn_vdec_get_out_info ") + self.attr.width = self.vdec_out_info.width + self.attr.height = self.vdec_out_info.height + + # 创建stream、event和ipe desc + self.send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.ipe_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.ipe_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.ipe_input_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_input_desc") + self.ipe_output_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_output_desc") + self.ipe_config, ret = sdk.lyn_ipe_create_config_desc() + common.error_check(ret, "lyn_ipe_create_config_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_reset_config_desc") + + # 获取模型信息 + scene_model_configs = [] + self.model_infos = [] + # self.model_infos = [ + # SceneModelHandler( + # model_plugin= '', + # plugin_path='', + # model_code='', + # model_name='', + # objects=[ + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code=''), + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code='') + # ], + # image_width=self.attr.width, + # image_height=self.attr.height, + # ) + # ] + self.model_nums = len(self.model_infos) + + + + self.batch_size = self.model_infos[0].batch_size if self.model_nums > 0 else 1 + self.model_width = self.model_infos[0].model_width if self.model_nums > 0 else 640 + self.model_height = self.model_infos[0].model_height if self.model_nums > 0 else 640 + # self.apu_output_size = self.model_infos[0].apu_output_size if self.model_nums > 0 else 1 + + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + # print(f'self.apu_output_size = {self.apu_output_size}') + + self.ipe_output_size = ( + self.model_width + * self.model_height + * (self.model_infos[0].model_desc.inputTensorAttrArray[0].dims[3] if self.model_nums > 0 else 3) + ) + + ( + self.resize_width, + self.resize_height, + self.pad_x, + self.pad_y, + ) = set_padding_data( + self.vdec_out_info.width, + self.vdec_out_info.height, + self.model_width, + self.model_height, + ) + + + # 创建对象池 + self.ipe_output_mem_pool = bufferpool.buffer_pool( + self.ipe_output_size * self.batch_size, 5 + ) + + self.venc_recv_pool = bufferpool.buffer_pool( + self.vdec_out_info.predict_buf_size, 5 + ) + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + + # 设置编码参数 + self.venc_attr = sdk.lyn_venc_attr_t() + ret = sdk.lyn_venc_set_default_params(self.venc_attr) + common.error_check(ret, "lyn_venc_set_default_params") + self.venc_attr.codec_type = sdk.lyn_codec_id_t.LYN_CODEC_ID_H264 + self.venc_attr.width = self.vdec_out_info.width + self.venc_attr.height = self.vdec_out_info.height + self.venc_attr.bit_depth = 8 + self.venc_attr.bframes_num = 0 + self.venc_attr.pframes_num = 5 + self.venc_attr.input_format = sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12 + self.venc_attr.target_bitrate = 6000000 + self.venc_attr.level = -1 + self.venc_handle, ret = sdk.lyn_venc_open(self.venc_attr) + common.error_check(ret, "lyn_vdec_open") + + self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + self.start_process_time = None + self.last_push_time = None + self.stream_out_process = None + self.init_output_process() + + + self.frame_step = 5 + self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + + + def init_output_process(self): + command = ['ffmpeg', + '-i', '-', + "-c:v", "copy", + '-f', 'rtsp', + '-rtsp_transport', 'tcp', + self.attr.output_path] + + # 启动FFmpeg子进程 + try: + self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError as e: + logger.exception(f"Failed to start process: {e}") + except subprocess.SubprocessError as e: + logger.exception(f"Subprocess error: {e}") + except Exception as e: + logger.exception(f"An unexpected error occurred: {e}") + + # stream_out_process = ( + # ffmpeg + # .input('pipe:', format='rawvideo', pix_fmt='bgr24', s='640x360', r=25) + # .output(output_path, vcodec='libx264', + # preset='ultrafast', tune='zerolatency', + # bitrate='1000k',maxrate='1000k', bufsize='2000k', f='rtsp',g=10) + # .run_async(pipe_stdin=True, pipe_stderr=True,overwrite_output=True, cmd=['ffmpeg', '-report']) + # ) + + def read_stderr(process): + for line in iter(process.stderr.readline, b''): + logger.info(f"stderr: {line.decode('utf-8').strip()}") + + if self.stream_out_process: + stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) + stderr_thread.daemon = True + stderr_thread.start() + + + def push_video(self): + # show window + global cancel_flag + + frame_rate = self.fps # 目标帧率 + frame_interval = 1 / frame_rate # 每帧间隔时间 + + if not self.last_push_time: + self.last_push_time = time.time() + + while True: + try: + frame = self.video_frame.get(False) + except: + time.sleep(0.01) + continue + if frame[1]: + # cv2.destroyAllWindows() + sys.exit() + + if frame and self.stream_out_process: + image = frame[0] + # resized_image = cv2.resize(image, (640, 360)) + + self.stream_out_process.stdin.write(image.tobytes()) + # elapsed_time = time.time() - self.start_process_time + elapsed_time = time.time() - self.last_push_time + sleep_time = frame_interval - elapsed_time - 0.01 + # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') + time.sleep(max(0, sleep_time)) + self.last_push_time = time.time() + + def run(self): + # 开启发送线程 + self.__send_thread = threading.Thread( + target=self.send_thread_func, args=() + ) + self.__send_thread.start() + + # 开启接收线程 + self.__recv_thread = threading.Thread( + target=self.recv_thread_func, args=() + ) + self.__recv_thread.start() + + # 开启图像处理和推理线程 + self.__ipe_thread = threading.Thread( + target=self.ipe_thread_func, args=() + ) + self.__ipe_thread.start() + + def close(self): + if self.__send_thread.is_alive(): + self.__send_thread.join() + if self.__recv_thread.is_alive(): + self.__recv_thread.join() + if self.__ipe_thread.is_alive(): + self.__ipe_thread.join() + + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.ipe_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.apu_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.plugin_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + ret = sdk.lyn_destroy_stream(self.send_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.ipe_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.apu_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.plugin_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_send_stream) + common.error_check(ret, "lyn_destroy_stream") + + ret = sdk.lyn_destroy_event(self.ipe_event) + common.error_check(ret, "lyn_destroy_event") + ret = sdk.lyn_destroy_event(self.apu_event) + common.error_check(ret, "lyn_destroy_event") + + # destory ipe desc and config + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_destroy_config_desc") + ret = sdk.lyn_plugin_unregister(self.plugin) + common.error_check(ret, "lyn_plugin_unregister") + + # 卸载模型 + + for model_info in self.model_infos: + ret = sdk.lyn_unload_model(model_info.model) + common.error_check(ret, "lyn_unload_model") + + if self.__vdec_hdl != "": + ret = sdk.lyn_vdec_close(self.__vdec_hdl) + common.error_check(ret, "lyn_vdec_close") + + if self.__ctx != "": + ret = sdk.lyn_destroy_context(self.__ctx) + common.error_check(ret, "lyn_destroy_context") + + + def send_thread_func(self): + # 设置上下文环境 创建发送stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + # 从解封装器读取一个包 + pkt, ret = sdk.lyn_demux_read_packet(self.__demux_hdl) + eos = pkt.eos + if ret != 0 and not eos: + sdk.lyn_demux_close(self.__demux_hdl) + time.sleep(500.0 / 1000) + logger.warning("demux failed, reconnecting...") + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + + if not ret: + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_synchronize_stream") + logger.debug(f'fps = {self.fps}') + continue + + # 发送给解码器解码 + ret = sdk.lyn_vdec_send_packet_async(self.send_stream, self.__vdec_hdl, pkt) + common.error_check(ret, "lyn_vdec_send_packet_async") + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + # 释放packet内存并通知接收结果 + if not eos: + sdk.lyn_demux_free_packet(pkt) + self.__send_num += 1 + self.__send_queue.put(self.__send_num) + else: + self.__send_queue.put(-1) + + + def recv_thread_func(self): + # 设置上下文环境 创建接收stream + sdk.lyn_set_current_context(self.__ctx) + frame_pool = bufferpool.buffer_pool(self.vdec_out_info.predict_buf_size, 5) + while self.__recv_num >= 0: + self.__recv_num = self.__send_queue.take() + + cb_data = recv_cb_data() + cb_data.frame.eos = self.__recv_num < 0 + cb_data.frame.data = frame_pool.pop() + cb_data.frame.size = self.vdec_out_info.predict_buf_size + cb_data.frame_pool = frame_pool + cb_data.send_num = self.__send_num + cb_data.recv_num = self.__recv_num + cb_data.attr = self.attr + cb_data.block_queue = self.__ipe_queue + cb_data.video_frame = self.video_frame + # 插入接收指令,并添加接收完成回调函数 + ret = sdk.lyn_vdec_recv_frame_async( + self.recv_stream, self.__vdec_hdl, cb_data.frame + ) + common.error_check(ret, "lyn_vdec_recv_frame_async") + ret = sdk.lyn_stream_add_async_callback( + self.recv_stream, recv_frame_cb, cb_data + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_thread_func(self): + # 设置上下文环境 创建ipe stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + cb_data = self.__ipe_queue.take() + eos = cb_data.frame.eos + self.start_process_time = time.time() + self.model_process(cb_data) # 这里的cb_data是从视频流解析出来的视频帧 + + def model_process(self, cb_data): + self.frame_idx = (self.frame_idx + 1) % self.frame_step + if self.frame_idx == 0: + # print(f'{self.frame_idx} process') + # 正常处理 + if self.model_nums > 0: + ipe_out_data = self.ipe_process(cb_data) + for model_info in self.model_infos: + apu_output_data = self.apu_process(model_info, ipe_out_data) + self.plugin_process(model_info, apu_output_data, cb_data) + + # ret = sdk.lyn_stream_add_async_callback( + # self.model_infos[-1].plugin_stream, + # show_video_cb, + # [cb_data, self.model_infos,self.last_alarm_time], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + + # self.merge_plugin_process(cb_data) + self.encode_process(cb_data) + + common.print_frame_rate(self.get_channel_info()) + + if self.model_nums > 0: + ret = sdk.lyn_stream_add_async_callback( + self.model_infos[-1].apu_stream, + free_to_pool_callback, + [self.ipe_output_mem_pool, ipe_out_data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + else: + # print(f'{self.frame_idx} draw') + # 跳过ipe apu处理,直接用上次的box渲染 + for model_info in self.model_infos: + self.plugin_draw_process(model_info, cb_data) + self.encode_process(cb_data) + common.print_frame_rate(self.get_channel_info()) + + def plugin_process(self,model_info, apu_output_data, cb_data): + pass + + def plugin_draw_process(self,model_info, cb_data): + format = int(sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12) + pythonapi.PyCapsule_GetPointer.restype = c_void_p + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + + frame_data_ptr = pythonapi.PyCapsule_GetPointer(cb_data.frame.data, None) + boxes_info_ptr = pythonapi.PyCapsule_GetPointer(model_info.boxes_info, None) + + draw_para = struct.pack( + 'P2IiP', + boxes_info_ptr, + self.codec_para.width, + self.codec_para.height, + format, + frame_data_ptr, + ) + ret = sdk.lyn_plugin_run_async( + model_info.plugin_stream, + model_info.plugin, + "lynDrawBoxAndText", + draw_para, + len(draw_para), + ) + common.error_check(ret, "lyn_plugin_run_async") + + def encode_process(self, cb_data: cb_data): + if self.model_nums > 0: + # 在 plugin 处理完之后,进行编码操作 + ret = sdk.lyn_record_event(self.model_infos[-1].plugin_stream, self.plugin_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(self.venc_send_stream, self.plugin_event) + common.error_check(ret, "lyn_stream_wait_event") + + if self.enc_head_flag: # 第一帧编码 + self.enc_head_flag = False + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.output_path = self.attr.output_path + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + + ret = sdk.lyn_venc_get_paramsset_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_get_paramsset_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_venc_sendframe_async( + self.venc_send_stream, self.venc_handle, cb_data.frame + ) + common.error_check(ret, "lyn_venc_sendframe_async") + + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.eos = cb_data.frame.eos + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.output_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + # ret = sdk.lyn_stream_add_async_callback( + # self.venc_send_stream, + # free_to_pool_callback, + # [cb_data.frame_pool, cb_data.frame.data], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + ret = sdk.lyn_venc_recvpacket_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_recvpacket_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, + free_to_pool_callback, + [cb_data.frame_pool, cb_data.frame.data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_process(self, cb_data: framepool_cb_data): + sdk.lyn_set_current_context(self.__ctx) + ipe_out_data = self.ipe_output_mem_pool.pop() + # 设置ipe输入 + ret = sdk.lyn_ipe_set_input_pic_desc( + self.ipe_input_desc, + cb_data.frame.data, + self.vdec_out_info.width, + self.vdec_out_info.height, + self.__vdec_attr.output_fmt, + ) + common.error_check(ret, "lyn_ipe_set_input_pic_desc") + ret = sdk.lyn_ipe_set_output_pic_data(self.ipe_output_desc, ipe_out_data) + common.error_check(ret, "lyn_ipe_set_output_pic_data") + ret = sdk.lyn_ipe_set_resize_config( + self.ipe_config, self.resize_width, self.resize_height + ) + common.error_check(ret, "lyn_ipe_set_resize_config") + ret = sdk.lyn_ipe_set_pad_config( + self.ipe_config, + self.pad_y, + self.pad_x, + self.pad_y, + self.pad_x, + 114, + 114, + 114, + ) + common.error_check(ret, "lyn_ipe_set_pad_config") + ret = sdk.lyn_ipe_set_c2c_config( + self.ipe_config, sdk.lyn_pixel_format_t.LYN_PIX_FMT_RGB24, 0 + ) + common.error_check(ret, "lyn_ipe_set_c2c_config") + ret = sdk.lyn_ipe_cal_output_pic_desc( + self.ipe_output_desc, self.ipe_input_desc, self.ipe_config, 0 + ) + common.error_check(ret, "lyn_ipe_cal_output_pic_desc") + ret = sdk.lyn_ipe_process_async( + self.ipe_stream, self.ipe_input_desc, self.ipe_output_desc, self.ipe_config + ) + common.error_check(ret, "lyn_ipe_process_async") + return ipe_out_data + + + def apu_process(self, model_info, ipe_out_data): + # 等待IPE处理完成 + ret = sdk.lyn_record_event(self.ipe_stream, self.ipe_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(model_info.apu_stream, self.ipe_event) + common.error_check(ret, "lyn_stream_wait_event") + apu_output_data = model_info.apu_output_mem_pool.pop() + ret = sdk.lyn_execute_model_async( + model_info.apu_stream, + model_info.model, + ipe_out_data, + apu_output_data, + self.batch_size, + ) + common.error_check(ret, "lyn_execute_model_async") + return apu_output_data + + def get_channel_info(self) -> str: + return f'{self.attr.device_id}_{self.attr.output_path}' \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so new file mode 100644 index 0000000..e066361 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 new file mode 100644 index 0000000..fa9aa0f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 Binary files differ diff --git a/rsa_utils.py b/rsa_utils.py new file mode 100644 index 0000000..a25fa2f --- /dev/null +++ b/rsa_utils.py @@ -0,0 +1,49 @@ +import base64 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.backends import default_backend + + +def encrypt_message_with_public_key(public_key_pem: str, message: str) -> str: + """ + 用公钥(字符串形式)对明文进行加密,并返回 base64 编码后的密文字符串 + """ + # 1. 从公钥字符串还原为公钥对象 + public_key = serialization.load_pem_public_key( + public_key_pem.encode("utf-8"), # 字符串转 bytes + backend=default_backend() + ) + + # 2. 对明文进行加密(OAEP 填充 + SHA256),返回二进制密文 + ciphertext_bytes = public_key.encrypt( + message.encode("utf-8"), + padding.PKCS1v15() + ) + + # 3. 将二进制密文转成 base64 字符串,方便通过 HTTP 或其他渠道传输 + ciphertext_base64 = base64.b64encode(ciphertext_bytes).decode("utf-8") + + return ciphertext_base64 + + +def decrypt_message_with_private_key(private_key_pem: str, ciphertext_base64: str) -> str: + """ + 用私钥(字符串形式)对 base64格式的密文进行解密,返回明文字符串 + """ + # 1. 从私钥字符串还原为私钥对象 + private_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), # 字符串转 bytes + password=None, # 如果在生成私钥时使用了加密算法,这里需要提供密码 + backend=default_backend() + ) + + # 2. Base64解码出二进制密文 + ciphertext_bytes = base64.b64decode(ciphertext_base64) + + # 3. 使用私钥解密 + plaintext_bytes = private_key.decrypt( + ciphertext_bytes, + padding.PKCS1v15() + ) + + return plaintext_bytes.decode("utf-8") \ No newline at end of file diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py new file mode 100644 index 0000000..83fc593 --- /dev/null +++ b/scene_handler/base_scene_handler.py @@ -0,0 +1,722 @@ +import struct +import numpy as np +import pylynchipsdk as sdk +import copy +import sys, os +import ctypes +from ctypes import * +import subprocess +import threading +import multiprocessing +import time + +from global_logger import logger +from blockqueue import block_queue +from callback_data_struct import * +import common +import bufferpool +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from info_query import ModelObject + +def save_file_cb(params): + cb_data: save_file_cb_data = params[0] + frame_cb_data: cb_data = params[1] + + packet = cb_data.packet + data_size, ret = sdk.lyn_enc_get_remote_packet_valid_size(packet) + common.error_check(ret, "lyn_enc_get_remote_packet_valid_size") + data = np.zeros(data_size, dtype=np.byte) + data_ptr = sdk.lyn_numpy_to_ptr(data) + ret = sdk.lyn_memcpy( + data_ptr, packet.data, data_size, sdk.lyn_memcpy_dir_t.ServerToClient + ) + common.error_check(ret, "lyn_memcpy") + + cb_data.video_frame.put([data, packet.eos]) + cb_data.recv_pool.push(packet.data) + return ret + +class SceneModelHandler: + def __init__(self, model_path, plugin_path, model_name,model_code, objects, image_width, image_height) -> None: + self.model_path = model_path + self.plugin_path = plugin_path + self.model_name = model_name + self.model_code = model_code + self.objects = objects + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + + self.model, ret = sdk.lyn_load_model(self.model_path) + common.error_check(ret, "lyn_load_model") + + self.plugin, ret = sdk.lyn_plugin_register(self.plugin_path) + common.error_check(ret, "lyn_plugin_register") + + self.apu_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.plugin_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.apu_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.model_desc, ret = sdk.lyn_model_get_desc(self.model) + common.error_check(ret, "lyn_model_get_desc") + self.batch_size = self.model_desc.inputTensorAttrArray[0].batchSize + self.model_width = self.model_desc.inputTensorAttrArray[0].dims[2] + self.model_height = self.model_desc.inputTensorAttrArray[0].dims[1] + + self.apu_output_size = self.model_desc.outputDataLen + + self.class_num = self.model_desc.outputTensorAttrArray[0].dims[3] - 5 + self.anchor_size = self.model_desc.outputTensorAttrArray[0].dims[1] + self.boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + + self.sub_objects_info, ret = sdk.lyn_malloc(ctypes.sizeof(SubObject)) + + sub_obj = sdk.c_malloc(ctypes.sizeof(SubObject)) + + # 获取底层指针 + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + pythonapi.PyCapsule_GetPointer.restype = c_void_p + raw_ptr = ctypes.pythonapi.PyCapsule_GetPointer(sub_obj, None) + if not raw_ptr: + raise ValueError("Failed to extract pointer from PyCapsule") + + # 转换为 ctypes.POINTER(SubObject) + sub_obj_ptr = ctypes.cast(raw_ptr, ctypes.POINTER(SubObject)) + + # 通过 ctypes.POINTER 操作数据 + sub_obj_instance = sub_obj_ptr.contents + sub_obj_instance.objectsnum = len(self.objects) + + for i in range(sub_obj_instance.objectsnum): + obj = self.objects[i] + sub_obj_instance.objects[i].object_id = int(obj.object_code) + sub_obj_instance.objects[i].object_name = obj.object_name + sub_obj_instance.objects[i].conf_threshold = obj.conf_threshold + sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold + sub_obj_instance.objects[i].model_code = obj.model_code + if obj.range: + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + abs_range = [] + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') + sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) + else: + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) + + ret = sdk.lyn_memcpy( + self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer + ) + common.error_check(ret, "save_boxinfo_cb lyn_memcpy") + + self.apu_output_mem_pool = bufferpool.buffer_pool( + self.apu_output_size * self.batch_size, 5 + ) + + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') + +class BaseSceneHandler: + def __init__(self, attr: device_process_attr) -> None: + self.attr = "" + self.__ctx = "" + self.__send_thread = "" + self.__recv_thread = "" + self.__ipe_thread = "" + self.__demux_hdl = "" + self.codec_para = "" + self.__vdec_hdl = "" + self.__vdec_attr = sdk.lyn_vdec_attr_t() + self.__send_queue = block_queue() + self.__ipe_queue = block_queue() + self.__send_num = 0 + self.__recv_num = 0 + # 创建上下文环境 + self.attr = copy.copy(attr) + self.video_frame = attr.video_frame + self.__ctx, ret = sdk.lyn_create_context(self.attr.device_id) + self.enc_head_flag = True + common.error_check(ret, "create context") + ret = sdk.lyn_register_error_handler(common.default_stream_error_handle) + common.error_check(ret, 'lyn_register_error_handler') + + # 打开解封装器 + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + common.error_check(ret, "lyn_demux_open ") + self.codec_para, ret = sdk.lyn_demux_get_codec_para(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_codec_para ") + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_framerate") + logger.debug(f'fps = {self.fps}') + + # 打开解码器 + self.__vdec_attr.codec_id = self.codec_para.codec_id + self.__vdec_attr.output_fmt = self.attr.output_fmt + self.__vdec_attr.scale = self.attr.scale + self.__vdec_hdl, ret = sdk.lyn_vdec_open(self.__vdec_attr) + common.error_check(ret, "lyn_vdec_open ") + self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( + self.codec_para, self.__vdec_attr + ) + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + common.error_check(ret, "lyn_vdec_get_out_info ") + self.attr.width = self.vdec_out_info.width + self.attr.height = self.vdec_out_info.height + + # 创建stream、event和ipe desc + self.send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.ipe_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.ipe_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.ipe_input_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_input_desc") + self.ipe_output_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_output_desc") + self.ipe_config, ret = sdk.lyn_ipe_create_config_desc() + common.error_check(ret, "lyn_ipe_create_config_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_reset_config_desc") + + # 获取模型信息 + scene_model_configs = [] + self.model_infos = [] + # self.model_infos = [ + # SceneModelHandler( + # model_plugin= '', + # plugin_path='', + # model_code='', + # model_name='', + # objects=[ + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code=''), + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code='') + # ], + # image_width=self.attr.width, + # image_height=self.attr.height, + # ) + # ] + self.model_nums = len(self.model_infos) + + + + self.batch_size = self.model_infos[0].batch_size if self.model_nums > 0 else 1 + self.model_width = self.model_infos[0].model_width if self.model_nums > 0 else 640 + self.model_height = self.model_infos[0].model_height if self.model_nums > 0 else 640 + # self.apu_output_size = self.model_infos[0].apu_output_size if self.model_nums > 0 else 1 + + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + # print(f'self.apu_output_size = {self.apu_output_size}') + + self.ipe_output_size = ( + self.model_width + * self.model_height + * (self.model_infos[0].model_desc.inputTensorAttrArray[0].dims[3] if self.model_nums > 0 else 3) + ) + + ( + self.resize_width, + self.resize_height, + self.pad_x, + self.pad_y, + ) = set_padding_data( + self.vdec_out_info.width, + self.vdec_out_info.height, + self.model_width, + self.model_height, + ) + + + # 创建对象池 + self.ipe_output_mem_pool = bufferpool.buffer_pool( + self.ipe_output_size * self.batch_size, 5 + ) + + self.venc_recv_pool = bufferpool.buffer_pool( + self.vdec_out_info.predict_buf_size, 5 + ) + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + + # 设置编码参数 + self.venc_attr = sdk.lyn_venc_attr_t() + ret = sdk.lyn_venc_set_default_params(self.venc_attr) + common.error_check(ret, "lyn_venc_set_default_params") + self.venc_attr.codec_type = sdk.lyn_codec_id_t.LYN_CODEC_ID_H264 + self.venc_attr.width = self.vdec_out_info.width + self.venc_attr.height = self.vdec_out_info.height + self.venc_attr.bit_depth = 8 + self.venc_attr.bframes_num = 0 + self.venc_attr.pframes_num = 5 + self.venc_attr.input_format = sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12 + self.venc_attr.target_bitrate = 6000000 + self.venc_attr.level = -1 + self.venc_handle, ret = sdk.lyn_venc_open(self.venc_attr) + common.error_check(ret, "lyn_vdec_open") + + self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + self.start_process_time = None + self.last_push_time = None + self.stream_out_process = None + self.init_output_process() + + + self.frame_step = 5 + self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + + + def init_output_process(self): + command = ['ffmpeg', + '-i', '-', + "-c:v", "copy", + '-f', 'rtsp', + '-rtsp_transport', 'tcp', + self.attr.output_path] + + # 启动FFmpeg子进程 + try: + self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError as e: + logger.exception(f"Failed to start process: {e}") + except subprocess.SubprocessError as e: + logger.exception(f"Subprocess error: {e}") + except Exception as e: + logger.exception(f"An unexpected error occurred: {e}") + + # stream_out_process = ( + # ffmpeg + # .input('pipe:', format='rawvideo', pix_fmt='bgr24', s='640x360', r=25) + # .output(output_path, vcodec='libx264', + # preset='ultrafast', tune='zerolatency', + # bitrate='1000k',maxrate='1000k', bufsize='2000k', f='rtsp',g=10) + # .run_async(pipe_stdin=True, pipe_stderr=True,overwrite_output=True, cmd=['ffmpeg', '-report']) + # ) + + def read_stderr(process): + for line in iter(process.stderr.readline, b''): + logger.info(f"stderr: {line.decode('utf-8').strip()}") + + if self.stream_out_process: + stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) + stderr_thread.daemon = True + stderr_thread.start() + + + def push_video(self): + # show window + global cancel_flag + + frame_rate = self.fps # 目标帧率 + frame_interval = 1 / frame_rate # 每帧间隔时间 + + if not self.last_push_time: + self.last_push_time = time.time() + + while True: + try: + frame = self.video_frame.get(False) + except: + time.sleep(0.01) + continue + if frame[1]: + # cv2.destroyAllWindows() + sys.exit() + + if frame and self.stream_out_process: + image = frame[0] + # resized_image = cv2.resize(image, (640, 360)) + + self.stream_out_process.stdin.write(image.tobytes()) + # elapsed_time = time.time() - self.start_process_time + elapsed_time = time.time() - self.last_push_time + sleep_time = frame_interval - elapsed_time - 0.01 + # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') + time.sleep(max(0, sleep_time)) + self.last_push_time = time.time() + + def run(self): + # 开启发送线程 + self.__send_thread = threading.Thread( + target=self.send_thread_func, args=() + ) + self.__send_thread.start() + + # 开启接收线程 + self.__recv_thread = threading.Thread( + target=self.recv_thread_func, args=() + ) + self.__recv_thread.start() + + # 开启图像处理和推理线程 + self.__ipe_thread = threading.Thread( + target=self.ipe_thread_func, args=() + ) + self.__ipe_thread.start() + + def close(self): + if self.__send_thread.is_alive(): + self.__send_thread.join() + if self.__recv_thread.is_alive(): + self.__recv_thread.join() + if self.__ipe_thread.is_alive(): + self.__ipe_thread.join() + + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.ipe_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.apu_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.plugin_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + ret = sdk.lyn_destroy_stream(self.send_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.ipe_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.apu_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.plugin_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_send_stream) + common.error_check(ret, "lyn_destroy_stream") + + ret = sdk.lyn_destroy_event(self.ipe_event) + common.error_check(ret, "lyn_destroy_event") + ret = sdk.lyn_destroy_event(self.apu_event) + common.error_check(ret, "lyn_destroy_event") + + # destory ipe desc and config + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_destroy_config_desc") + ret = sdk.lyn_plugin_unregister(self.plugin) + common.error_check(ret, "lyn_plugin_unregister") + + # 卸载模型 + + for model_info in self.model_infos: + ret = sdk.lyn_unload_model(model_info.model) + common.error_check(ret, "lyn_unload_model") + + if self.__vdec_hdl != "": + ret = sdk.lyn_vdec_close(self.__vdec_hdl) + common.error_check(ret, "lyn_vdec_close") + + if self.__ctx != "": + ret = sdk.lyn_destroy_context(self.__ctx) + common.error_check(ret, "lyn_destroy_context") + + + def send_thread_func(self): + # 设置上下文环境 创建发送stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + # 从解封装器读取一个包 + pkt, ret = sdk.lyn_demux_read_packet(self.__demux_hdl) + eos = pkt.eos + if ret != 0 and not eos: + sdk.lyn_demux_close(self.__demux_hdl) + time.sleep(500.0 / 1000) + logger.warning("demux failed, reconnecting...") + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + + if not ret: + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_synchronize_stream") + logger.debug(f'fps = {self.fps}') + continue + + # 发送给解码器解码 + ret = sdk.lyn_vdec_send_packet_async(self.send_stream, self.__vdec_hdl, pkt) + common.error_check(ret, "lyn_vdec_send_packet_async") + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + # 释放packet内存并通知接收结果 + if not eos: + sdk.lyn_demux_free_packet(pkt) + self.__send_num += 1 + self.__send_queue.put(self.__send_num) + else: + self.__send_queue.put(-1) + + + def recv_thread_func(self): + # 设置上下文环境 创建接收stream + sdk.lyn_set_current_context(self.__ctx) + frame_pool = bufferpool.buffer_pool(self.vdec_out_info.predict_buf_size, 5) + while self.__recv_num >= 0: + self.__recv_num = self.__send_queue.take() + + cb_data = recv_cb_data() + cb_data.frame.eos = self.__recv_num < 0 + cb_data.frame.data = frame_pool.pop() + cb_data.frame.size = self.vdec_out_info.predict_buf_size + cb_data.frame_pool = frame_pool + cb_data.send_num = self.__send_num + cb_data.recv_num = self.__recv_num + cb_data.attr = self.attr + cb_data.block_queue = self.__ipe_queue + cb_data.video_frame = self.video_frame + # 插入接收指令,并添加接收完成回调函数 + ret = sdk.lyn_vdec_recv_frame_async( + self.recv_stream, self.__vdec_hdl, cb_data.frame + ) + common.error_check(ret, "lyn_vdec_recv_frame_async") + ret = sdk.lyn_stream_add_async_callback( + self.recv_stream, recv_frame_cb, cb_data + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_thread_func(self): + # 设置上下文环境 创建ipe stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + cb_data = self.__ipe_queue.take() + eos = cb_data.frame.eos + self.start_process_time = time.time() + self.model_process(cb_data) # 这里的cb_data是从视频流解析出来的视频帧 + + def model_process(self, cb_data): + self.frame_idx = (self.frame_idx + 1) % self.frame_step + if self.frame_idx == 0: + # print(f'{self.frame_idx} process') + # 正常处理 + if self.model_nums > 0: + ipe_out_data = self.ipe_process(cb_data) + for model_info in self.model_infos: + apu_output_data = self.apu_process(model_info, ipe_out_data) + self.plugin_process(model_info, apu_output_data, cb_data) + + # ret = sdk.lyn_stream_add_async_callback( + # self.model_infos[-1].plugin_stream, + # show_video_cb, + # [cb_data, self.model_infos,self.last_alarm_time], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + + # self.merge_plugin_process(cb_data) + self.encode_process(cb_data) + + common.print_frame_rate(self.get_channel_info()) + + if self.model_nums > 0: + ret = sdk.lyn_stream_add_async_callback( + self.model_infos[-1].apu_stream, + free_to_pool_callback, + [self.ipe_output_mem_pool, ipe_out_data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + else: + # print(f'{self.frame_idx} draw') + # 跳过ipe apu处理,直接用上次的box渲染 + for model_info in self.model_infos: + self.plugin_draw_process(model_info, cb_data) + self.encode_process(cb_data) + common.print_frame_rate(self.get_channel_info()) + + def plugin_process(self,model_info, apu_output_data, cb_data): + pass + + def plugin_draw_process(self,model_info, cb_data): + format = int(sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12) + pythonapi.PyCapsule_GetPointer.restype = c_void_p + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + + frame_data_ptr = pythonapi.PyCapsule_GetPointer(cb_data.frame.data, None) + boxes_info_ptr = pythonapi.PyCapsule_GetPointer(model_info.boxes_info, None) + + draw_para = struct.pack( + 'P2IiP', + boxes_info_ptr, + self.codec_para.width, + self.codec_para.height, + format, + frame_data_ptr, + ) + ret = sdk.lyn_plugin_run_async( + model_info.plugin_stream, + model_info.plugin, + "lynDrawBoxAndText", + draw_para, + len(draw_para), + ) + common.error_check(ret, "lyn_plugin_run_async") + + def encode_process(self, cb_data: cb_data): + if self.model_nums > 0: + # 在 plugin 处理完之后,进行编码操作 + ret = sdk.lyn_record_event(self.model_infos[-1].plugin_stream, self.plugin_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(self.venc_send_stream, self.plugin_event) + common.error_check(ret, "lyn_stream_wait_event") + + if self.enc_head_flag: # 第一帧编码 + self.enc_head_flag = False + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.output_path = self.attr.output_path + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + + ret = sdk.lyn_venc_get_paramsset_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_get_paramsset_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_venc_sendframe_async( + self.venc_send_stream, self.venc_handle, cb_data.frame + ) + common.error_check(ret, "lyn_venc_sendframe_async") + + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.eos = cb_data.frame.eos + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.output_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + # ret = sdk.lyn_stream_add_async_callback( + # self.venc_send_stream, + # free_to_pool_callback, + # [cb_data.frame_pool, cb_data.frame.data], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + ret = sdk.lyn_venc_recvpacket_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_recvpacket_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, + free_to_pool_callback, + [cb_data.frame_pool, cb_data.frame.data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_process(self, cb_data: framepool_cb_data): + sdk.lyn_set_current_context(self.__ctx) + ipe_out_data = self.ipe_output_mem_pool.pop() + # 设置ipe输入 + ret = sdk.lyn_ipe_set_input_pic_desc( + self.ipe_input_desc, + cb_data.frame.data, + self.vdec_out_info.width, + self.vdec_out_info.height, + self.__vdec_attr.output_fmt, + ) + common.error_check(ret, "lyn_ipe_set_input_pic_desc") + ret = sdk.lyn_ipe_set_output_pic_data(self.ipe_output_desc, ipe_out_data) + common.error_check(ret, "lyn_ipe_set_output_pic_data") + ret = sdk.lyn_ipe_set_resize_config( + self.ipe_config, self.resize_width, self.resize_height + ) + common.error_check(ret, "lyn_ipe_set_resize_config") + ret = sdk.lyn_ipe_set_pad_config( + self.ipe_config, + self.pad_y, + self.pad_x, + self.pad_y, + self.pad_x, + 114, + 114, + 114, + ) + common.error_check(ret, "lyn_ipe_set_pad_config") + ret = sdk.lyn_ipe_set_c2c_config( + self.ipe_config, sdk.lyn_pixel_format_t.LYN_PIX_FMT_RGB24, 0 + ) + common.error_check(ret, "lyn_ipe_set_c2c_config") + ret = sdk.lyn_ipe_cal_output_pic_desc( + self.ipe_output_desc, self.ipe_input_desc, self.ipe_config, 0 + ) + common.error_check(ret, "lyn_ipe_cal_output_pic_desc") + ret = sdk.lyn_ipe_process_async( + self.ipe_stream, self.ipe_input_desc, self.ipe_output_desc, self.ipe_config + ) + common.error_check(ret, "lyn_ipe_process_async") + return ipe_out_data + + + def apu_process(self, model_info, ipe_out_data): + # 等待IPE处理完成 + ret = sdk.lyn_record_event(self.ipe_stream, self.ipe_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(model_info.apu_stream, self.ipe_event) + common.error_check(ret, "lyn_stream_wait_event") + apu_output_data = model_info.apu_output_mem_pool.pop() + ret = sdk.lyn_execute_model_async( + model_info.apu_stream, + model_info.model, + ipe_out_data, + apu_output_data, + self.batch_size, + ) + common.error_check(ret, "lyn_execute_model_async") + return apu_output_data + + def get_channel_info(self) -> str: + return f'{self.attr.device_id}_{self.attr.output_path}' \ No newline at end of file diff --git a/scene_runner.py b/scene_runner.py new file mode 100644 index 0000000..76241cd --- /dev/null +++ b/scene_runner.py @@ -0,0 +1,45 @@ +import threading +import queue +import sys, os + +import common +import bufferpool +from blockqueue import block_queue +from callback_data_struct import * +from info_query import SceneInfo +from string_utils import camel_to_snake, get_class, snake_to_camel +from global_logger import logger + +# 需要全局的http tool? +def run_scene_device(device_no:str, input_url: str, output_url:str, device_id: int, scene_info:SceneInfo, alarm_interval, threads) -> None: + attr = device_process_attr() + attr.device_no = device_no + attr.url = input_url + attr.output_path = output_url + attr.device_id = device_id + attr.alarm_interval = alarm_interval + attr.video_frame = queue.Queue(10) + attr.scene_info = scene_info + if not attr.url: + raise ValueError('input file miss!!!') + if not attr.output_path: + raise ValueError('unspecified output path!!!') + + handle_task_name = scene_info.handle_task + if handle_task_name: + try: + handler_cls = get_class(f'scene_handler.{handle_task_name}', snake_to_camel(handle_task_name)) + scene_instance = handler_cls(attr) + + infer_thread = threading.Thread(target=scene_instance.run, args=()) + threads.append(infer_thread) + infer_thread.start() + + out_thread = threading.Thread(target=scene_instance.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + # logger.exception(e) + logger.exception(f'scene start failed: {e}') + else: + logger.warning(f'scene {scene_info.remark} for device {device_no} start failed beause no handle task') \ No newline at end of file diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so new file mode 100644 index 0000000..e066361 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 new file mode 100644 index 0000000..fa9aa0f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 Binary files differ diff --git a/rsa_utils.py b/rsa_utils.py new file mode 100644 index 0000000..a25fa2f --- /dev/null +++ b/rsa_utils.py @@ -0,0 +1,49 @@ +import base64 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.backends import default_backend + + +def encrypt_message_with_public_key(public_key_pem: str, message: str) -> str: + """ + 用公钥(字符串形式)对明文进行加密,并返回 base64 编码后的密文字符串 + """ + # 1. 从公钥字符串还原为公钥对象 + public_key = serialization.load_pem_public_key( + public_key_pem.encode("utf-8"), # 字符串转 bytes + backend=default_backend() + ) + + # 2. 对明文进行加密(OAEP 填充 + SHA256),返回二进制密文 + ciphertext_bytes = public_key.encrypt( + message.encode("utf-8"), + padding.PKCS1v15() + ) + + # 3. 将二进制密文转成 base64 字符串,方便通过 HTTP 或其他渠道传输 + ciphertext_base64 = base64.b64encode(ciphertext_bytes).decode("utf-8") + + return ciphertext_base64 + + +def decrypt_message_with_private_key(private_key_pem: str, ciphertext_base64: str) -> str: + """ + 用私钥(字符串形式)对 base64格式的密文进行解密,返回明文字符串 + """ + # 1. 从私钥字符串还原为私钥对象 + private_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), # 字符串转 bytes + password=None, # 如果在生成私钥时使用了加密算法,这里需要提供密码 + backend=default_backend() + ) + + # 2. Base64解码出二进制密文 + ciphertext_bytes = base64.b64decode(ciphertext_base64) + + # 3. 使用私钥解密 + plaintext_bytes = private_key.decrypt( + ciphertext_bytes, + padding.PKCS1v15() + ) + + return plaintext_bytes.decode("utf-8") \ No newline at end of file diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py new file mode 100644 index 0000000..83fc593 --- /dev/null +++ b/scene_handler/base_scene_handler.py @@ -0,0 +1,722 @@ +import struct +import numpy as np +import pylynchipsdk as sdk +import copy +import sys, os +import ctypes +from ctypes import * +import subprocess +import threading +import multiprocessing +import time + +from global_logger import logger +from blockqueue import block_queue +from callback_data_struct import * +import common +import bufferpool +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from info_query import ModelObject + +def save_file_cb(params): + cb_data: save_file_cb_data = params[0] + frame_cb_data: cb_data = params[1] + + packet = cb_data.packet + data_size, ret = sdk.lyn_enc_get_remote_packet_valid_size(packet) + common.error_check(ret, "lyn_enc_get_remote_packet_valid_size") + data = np.zeros(data_size, dtype=np.byte) + data_ptr = sdk.lyn_numpy_to_ptr(data) + ret = sdk.lyn_memcpy( + data_ptr, packet.data, data_size, sdk.lyn_memcpy_dir_t.ServerToClient + ) + common.error_check(ret, "lyn_memcpy") + + cb_data.video_frame.put([data, packet.eos]) + cb_data.recv_pool.push(packet.data) + return ret + +class SceneModelHandler: + def __init__(self, model_path, plugin_path, model_name,model_code, objects, image_width, image_height) -> None: + self.model_path = model_path + self.plugin_path = plugin_path + self.model_name = model_name + self.model_code = model_code + self.objects = objects + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + + self.model, ret = sdk.lyn_load_model(self.model_path) + common.error_check(ret, "lyn_load_model") + + self.plugin, ret = sdk.lyn_plugin_register(self.plugin_path) + common.error_check(ret, "lyn_plugin_register") + + self.apu_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.plugin_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.apu_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.model_desc, ret = sdk.lyn_model_get_desc(self.model) + common.error_check(ret, "lyn_model_get_desc") + self.batch_size = self.model_desc.inputTensorAttrArray[0].batchSize + self.model_width = self.model_desc.inputTensorAttrArray[0].dims[2] + self.model_height = self.model_desc.inputTensorAttrArray[0].dims[1] + + self.apu_output_size = self.model_desc.outputDataLen + + self.class_num = self.model_desc.outputTensorAttrArray[0].dims[3] - 5 + self.anchor_size = self.model_desc.outputTensorAttrArray[0].dims[1] + self.boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + + self.sub_objects_info, ret = sdk.lyn_malloc(ctypes.sizeof(SubObject)) + + sub_obj = sdk.c_malloc(ctypes.sizeof(SubObject)) + + # 获取底层指针 + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + pythonapi.PyCapsule_GetPointer.restype = c_void_p + raw_ptr = ctypes.pythonapi.PyCapsule_GetPointer(sub_obj, None) + if not raw_ptr: + raise ValueError("Failed to extract pointer from PyCapsule") + + # 转换为 ctypes.POINTER(SubObject) + sub_obj_ptr = ctypes.cast(raw_ptr, ctypes.POINTER(SubObject)) + + # 通过 ctypes.POINTER 操作数据 + sub_obj_instance = sub_obj_ptr.contents + sub_obj_instance.objectsnum = len(self.objects) + + for i in range(sub_obj_instance.objectsnum): + obj = self.objects[i] + sub_obj_instance.objects[i].object_id = int(obj.object_code) + sub_obj_instance.objects[i].object_name = obj.object_name + sub_obj_instance.objects[i].conf_threshold = obj.conf_threshold + sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold + sub_obj_instance.objects[i].model_code = obj.model_code + if obj.range: + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + abs_range = [] + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') + sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) + else: + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) + + ret = sdk.lyn_memcpy( + self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer + ) + common.error_check(ret, "save_boxinfo_cb lyn_memcpy") + + self.apu_output_mem_pool = bufferpool.buffer_pool( + self.apu_output_size * self.batch_size, 5 + ) + + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') + +class BaseSceneHandler: + def __init__(self, attr: device_process_attr) -> None: + self.attr = "" + self.__ctx = "" + self.__send_thread = "" + self.__recv_thread = "" + self.__ipe_thread = "" + self.__demux_hdl = "" + self.codec_para = "" + self.__vdec_hdl = "" + self.__vdec_attr = sdk.lyn_vdec_attr_t() + self.__send_queue = block_queue() + self.__ipe_queue = block_queue() + self.__send_num = 0 + self.__recv_num = 0 + # 创建上下文环境 + self.attr = copy.copy(attr) + self.video_frame = attr.video_frame + self.__ctx, ret = sdk.lyn_create_context(self.attr.device_id) + self.enc_head_flag = True + common.error_check(ret, "create context") + ret = sdk.lyn_register_error_handler(common.default_stream_error_handle) + common.error_check(ret, 'lyn_register_error_handler') + + # 打开解封装器 + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + common.error_check(ret, "lyn_demux_open ") + self.codec_para, ret = sdk.lyn_demux_get_codec_para(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_codec_para ") + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_framerate") + logger.debug(f'fps = {self.fps}') + + # 打开解码器 + self.__vdec_attr.codec_id = self.codec_para.codec_id + self.__vdec_attr.output_fmt = self.attr.output_fmt + self.__vdec_attr.scale = self.attr.scale + self.__vdec_hdl, ret = sdk.lyn_vdec_open(self.__vdec_attr) + common.error_check(ret, "lyn_vdec_open ") + self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( + self.codec_para, self.__vdec_attr + ) + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + common.error_check(ret, "lyn_vdec_get_out_info ") + self.attr.width = self.vdec_out_info.width + self.attr.height = self.vdec_out_info.height + + # 创建stream、event和ipe desc + self.send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.ipe_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.ipe_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.ipe_input_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_input_desc") + self.ipe_output_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_output_desc") + self.ipe_config, ret = sdk.lyn_ipe_create_config_desc() + common.error_check(ret, "lyn_ipe_create_config_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_reset_config_desc") + + # 获取模型信息 + scene_model_configs = [] + self.model_infos = [] + # self.model_infos = [ + # SceneModelHandler( + # model_plugin= '', + # plugin_path='', + # model_code='', + # model_name='', + # objects=[ + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code=''), + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code='') + # ], + # image_width=self.attr.width, + # image_height=self.attr.height, + # ) + # ] + self.model_nums = len(self.model_infos) + + + + self.batch_size = self.model_infos[0].batch_size if self.model_nums > 0 else 1 + self.model_width = self.model_infos[0].model_width if self.model_nums > 0 else 640 + self.model_height = self.model_infos[0].model_height if self.model_nums > 0 else 640 + # self.apu_output_size = self.model_infos[0].apu_output_size if self.model_nums > 0 else 1 + + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + # print(f'self.apu_output_size = {self.apu_output_size}') + + self.ipe_output_size = ( + self.model_width + * self.model_height + * (self.model_infos[0].model_desc.inputTensorAttrArray[0].dims[3] if self.model_nums > 0 else 3) + ) + + ( + self.resize_width, + self.resize_height, + self.pad_x, + self.pad_y, + ) = set_padding_data( + self.vdec_out_info.width, + self.vdec_out_info.height, + self.model_width, + self.model_height, + ) + + + # 创建对象池 + self.ipe_output_mem_pool = bufferpool.buffer_pool( + self.ipe_output_size * self.batch_size, 5 + ) + + self.venc_recv_pool = bufferpool.buffer_pool( + self.vdec_out_info.predict_buf_size, 5 + ) + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + + # 设置编码参数 + self.venc_attr = sdk.lyn_venc_attr_t() + ret = sdk.lyn_venc_set_default_params(self.venc_attr) + common.error_check(ret, "lyn_venc_set_default_params") + self.venc_attr.codec_type = sdk.lyn_codec_id_t.LYN_CODEC_ID_H264 + self.venc_attr.width = self.vdec_out_info.width + self.venc_attr.height = self.vdec_out_info.height + self.venc_attr.bit_depth = 8 + self.venc_attr.bframes_num = 0 + self.venc_attr.pframes_num = 5 + self.venc_attr.input_format = sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12 + self.venc_attr.target_bitrate = 6000000 + self.venc_attr.level = -1 + self.venc_handle, ret = sdk.lyn_venc_open(self.venc_attr) + common.error_check(ret, "lyn_vdec_open") + + self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + self.start_process_time = None + self.last_push_time = None + self.stream_out_process = None + self.init_output_process() + + + self.frame_step = 5 + self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + + + def init_output_process(self): + command = ['ffmpeg', + '-i', '-', + "-c:v", "copy", + '-f', 'rtsp', + '-rtsp_transport', 'tcp', + self.attr.output_path] + + # 启动FFmpeg子进程 + try: + self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError as e: + logger.exception(f"Failed to start process: {e}") + except subprocess.SubprocessError as e: + logger.exception(f"Subprocess error: {e}") + except Exception as e: + logger.exception(f"An unexpected error occurred: {e}") + + # stream_out_process = ( + # ffmpeg + # .input('pipe:', format='rawvideo', pix_fmt='bgr24', s='640x360', r=25) + # .output(output_path, vcodec='libx264', + # preset='ultrafast', tune='zerolatency', + # bitrate='1000k',maxrate='1000k', bufsize='2000k', f='rtsp',g=10) + # .run_async(pipe_stdin=True, pipe_stderr=True,overwrite_output=True, cmd=['ffmpeg', '-report']) + # ) + + def read_stderr(process): + for line in iter(process.stderr.readline, b''): + logger.info(f"stderr: {line.decode('utf-8').strip()}") + + if self.stream_out_process: + stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) + stderr_thread.daemon = True + stderr_thread.start() + + + def push_video(self): + # show window + global cancel_flag + + frame_rate = self.fps # 目标帧率 + frame_interval = 1 / frame_rate # 每帧间隔时间 + + if not self.last_push_time: + self.last_push_time = time.time() + + while True: + try: + frame = self.video_frame.get(False) + except: + time.sleep(0.01) + continue + if frame[1]: + # cv2.destroyAllWindows() + sys.exit() + + if frame and self.stream_out_process: + image = frame[0] + # resized_image = cv2.resize(image, (640, 360)) + + self.stream_out_process.stdin.write(image.tobytes()) + # elapsed_time = time.time() - self.start_process_time + elapsed_time = time.time() - self.last_push_time + sleep_time = frame_interval - elapsed_time - 0.01 + # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') + time.sleep(max(0, sleep_time)) + self.last_push_time = time.time() + + def run(self): + # 开启发送线程 + self.__send_thread = threading.Thread( + target=self.send_thread_func, args=() + ) + self.__send_thread.start() + + # 开启接收线程 + self.__recv_thread = threading.Thread( + target=self.recv_thread_func, args=() + ) + self.__recv_thread.start() + + # 开启图像处理和推理线程 + self.__ipe_thread = threading.Thread( + target=self.ipe_thread_func, args=() + ) + self.__ipe_thread.start() + + def close(self): + if self.__send_thread.is_alive(): + self.__send_thread.join() + if self.__recv_thread.is_alive(): + self.__recv_thread.join() + if self.__ipe_thread.is_alive(): + self.__ipe_thread.join() + + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.ipe_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.apu_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.plugin_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + ret = sdk.lyn_destroy_stream(self.send_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.ipe_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.apu_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.plugin_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_send_stream) + common.error_check(ret, "lyn_destroy_stream") + + ret = sdk.lyn_destroy_event(self.ipe_event) + common.error_check(ret, "lyn_destroy_event") + ret = sdk.lyn_destroy_event(self.apu_event) + common.error_check(ret, "lyn_destroy_event") + + # destory ipe desc and config + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_destroy_config_desc") + ret = sdk.lyn_plugin_unregister(self.plugin) + common.error_check(ret, "lyn_plugin_unregister") + + # 卸载模型 + + for model_info in self.model_infos: + ret = sdk.lyn_unload_model(model_info.model) + common.error_check(ret, "lyn_unload_model") + + if self.__vdec_hdl != "": + ret = sdk.lyn_vdec_close(self.__vdec_hdl) + common.error_check(ret, "lyn_vdec_close") + + if self.__ctx != "": + ret = sdk.lyn_destroy_context(self.__ctx) + common.error_check(ret, "lyn_destroy_context") + + + def send_thread_func(self): + # 设置上下文环境 创建发送stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + # 从解封装器读取一个包 + pkt, ret = sdk.lyn_demux_read_packet(self.__demux_hdl) + eos = pkt.eos + if ret != 0 and not eos: + sdk.lyn_demux_close(self.__demux_hdl) + time.sleep(500.0 / 1000) + logger.warning("demux failed, reconnecting...") + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + + if not ret: + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_synchronize_stream") + logger.debug(f'fps = {self.fps}') + continue + + # 发送给解码器解码 + ret = sdk.lyn_vdec_send_packet_async(self.send_stream, self.__vdec_hdl, pkt) + common.error_check(ret, "lyn_vdec_send_packet_async") + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + # 释放packet内存并通知接收结果 + if not eos: + sdk.lyn_demux_free_packet(pkt) + self.__send_num += 1 + self.__send_queue.put(self.__send_num) + else: + self.__send_queue.put(-1) + + + def recv_thread_func(self): + # 设置上下文环境 创建接收stream + sdk.lyn_set_current_context(self.__ctx) + frame_pool = bufferpool.buffer_pool(self.vdec_out_info.predict_buf_size, 5) + while self.__recv_num >= 0: + self.__recv_num = self.__send_queue.take() + + cb_data = recv_cb_data() + cb_data.frame.eos = self.__recv_num < 0 + cb_data.frame.data = frame_pool.pop() + cb_data.frame.size = self.vdec_out_info.predict_buf_size + cb_data.frame_pool = frame_pool + cb_data.send_num = self.__send_num + cb_data.recv_num = self.__recv_num + cb_data.attr = self.attr + cb_data.block_queue = self.__ipe_queue + cb_data.video_frame = self.video_frame + # 插入接收指令,并添加接收完成回调函数 + ret = sdk.lyn_vdec_recv_frame_async( + self.recv_stream, self.__vdec_hdl, cb_data.frame + ) + common.error_check(ret, "lyn_vdec_recv_frame_async") + ret = sdk.lyn_stream_add_async_callback( + self.recv_stream, recv_frame_cb, cb_data + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_thread_func(self): + # 设置上下文环境 创建ipe stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + cb_data = self.__ipe_queue.take() + eos = cb_data.frame.eos + self.start_process_time = time.time() + self.model_process(cb_data) # 这里的cb_data是从视频流解析出来的视频帧 + + def model_process(self, cb_data): + self.frame_idx = (self.frame_idx + 1) % self.frame_step + if self.frame_idx == 0: + # print(f'{self.frame_idx} process') + # 正常处理 + if self.model_nums > 0: + ipe_out_data = self.ipe_process(cb_data) + for model_info in self.model_infos: + apu_output_data = self.apu_process(model_info, ipe_out_data) + self.plugin_process(model_info, apu_output_data, cb_data) + + # ret = sdk.lyn_stream_add_async_callback( + # self.model_infos[-1].plugin_stream, + # show_video_cb, + # [cb_data, self.model_infos,self.last_alarm_time], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + + # self.merge_plugin_process(cb_data) + self.encode_process(cb_data) + + common.print_frame_rate(self.get_channel_info()) + + if self.model_nums > 0: + ret = sdk.lyn_stream_add_async_callback( + self.model_infos[-1].apu_stream, + free_to_pool_callback, + [self.ipe_output_mem_pool, ipe_out_data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + else: + # print(f'{self.frame_idx} draw') + # 跳过ipe apu处理,直接用上次的box渲染 + for model_info in self.model_infos: + self.plugin_draw_process(model_info, cb_data) + self.encode_process(cb_data) + common.print_frame_rate(self.get_channel_info()) + + def plugin_process(self,model_info, apu_output_data, cb_data): + pass + + def plugin_draw_process(self,model_info, cb_data): + format = int(sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12) + pythonapi.PyCapsule_GetPointer.restype = c_void_p + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + + frame_data_ptr = pythonapi.PyCapsule_GetPointer(cb_data.frame.data, None) + boxes_info_ptr = pythonapi.PyCapsule_GetPointer(model_info.boxes_info, None) + + draw_para = struct.pack( + 'P2IiP', + boxes_info_ptr, + self.codec_para.width, + self.codec_para.height, + format, + frame_data_ptr, + ) + ret = sdk.lyn_plugin_run_async( + model_info.plugin_stream, + model_info.plugin, + "lynDrawBoxAndText", + draw_para, + len(draw_para), + ) + common.error_check(ret, "lyn_plugin_run_async") + + def encode_process(self, cb_data: cb_data): + if self.model_nums > 0: + # 在 plugin 处理完之后,进行编码操作 + ret = sdk.lyn_record_event(self.model_infos[-1].plugin_stream, self.plugin_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(self.venc_send_stream, self.plugin_event) + common.error_check(ret, "lyn_stream_wait_event") + + if self.enc_head_flag: # 第一帧编码 + self.enc_head_flag = False + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.output_path = self.attr.output_path + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + + ret = sdk.lyn_venc_get_paramsset_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_get_paramsset_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_venc_sendframe_async( + self.venc_send_stream, self.venc_handle, cb_data.frame + ) + common.error_check(ret, "lyn_venc_sendframe_async") + + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.eos = cb_data.frame.eos + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.output_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + # ret = sdk.lyn_stream_add_async_callback( + # self.venc_send_stream, + # free_to_pool_callback, + # [cb_data.frame_pool, cb_data.frame.data], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + ret = sdk.lyn_venc_recvpacket_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_recvpacket_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, + free_to_pool_callback, + [cb_data.frame_pool, cb_data.frame.data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_process(self, cb_data: framepool_cb_data): + sdk.lyn_set_current_context(self.__ctx) + ipe_out_data = self.ipe_output_mem_pool.pop() + # 设置ipe输入 + ret = sdk.lyn_ipe_set_input_pic_desc( + self.ipe_input_desc, + cb_data.frame.data, + self.vdec_out_info.width, + self.vdec_out_info.height, + self.__vdec_attr.output_fmt, + ) + common.error_check(ret, "lyn_ipe_set_input_pic_desc") + ret = sdk.lyn_ipe_set_output_pic_data(self.ipe_output_desc, ipe_out_data) + common.error_check(ret, "lyn_ipe_set_output_pic_data") + ret = sdk.lyn_ipe_set_resize_config( + self.ipe_config, self.resize_width, self.resize_height + ) + common.error_check(ret, "lyn_ipe_set_resize_config") + ret = sdk.lyn_ipe_set_pad_config( + self.ipe_config, + self.pad_y, + self.pad_x, + self.pad_y, + self.pad_x, + 114, + 114, + 114, + ) + common.error_check(ret, "lyn_ipe_set_pad_config") + ret = sdk.lyn_ipe_set_c2c_config( + self.ipe_config, sdk.lyn_pixel_format_t.LYN_PIX_FMT_RGB24, 0 + ) + common.error_check(ret, "lyn_ipe_set_c2c_config") + ret = sdk.lyn_ipe_cal_output_pic_desc( + self.ipe_output_desc, self.ipe_input_desc, self.ipe_config, 0 + ) + common.error_check(ret, "lyn_ipe_cal_output_pic_desc") + ret = sdk.lyn_ipe_process_async( + self.ipe_stream, self.ipe_input_desc, self.ipe_output_desc, self.ipe_config + ) + common.error_check(ret, "lyn_ipe_process_async") + return ipe_out_data + + + def apu_process(self, model_info, ipe_out_data): + # 等待IPE处理完成 + ret = sdk.lyn_record_event(self.ipe_stream, self.ipe_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(model_info.apu_stream, self.ipe_event) + common.error_check(ret, "lyn_stream_wait_event") + apu_output_data = model_info.apu_output_mem_pool.pop() + ret = sdk.lyn_execute_model_async( + model_info.apu_stream, + model_info.model, + ipe_out_data, + apu_output_data, + self.batch_size, + ) + common.error_check(ret, "lyn_execute_model_async") + return apu_output_data + + def get_channel_info(self) -> str: + return f'{self.attr.device_id}_{self.attr.output_path}' \ No newline at end of file diff --git a/scene_runner.py b/scene_runner.py new file mode 100644 index 0000000..76241cd --- /dev/null +++ b/scene_runner.py @@ -0,0 +1,45 @@ +import threading +import queue +import sys, os + +import common +import bufferpool +from blockqueue import block_queue +from callback_data_struct import * +from info_query import SceneInfo +from string_utils import camel_to_snake, get_class, snake_to_camel +from global_logger import logger + +# 需要全局的http tool? +def run_scene_device(device_no:str, input_url: str, output_url:str, device_id: int, scene_info:SceneInfo, alarm_interval, threads) -> None: + attr = device_process_attr() + attr.device_no = device_no + attr.url = input_url + attr.output_path = output_url + attr.device_id = device_id + attr.alarm_interval = alarm_interval + attr.video_frame = queue.Queue(10) + attr.scene_info = scene_info + if not attr.url: + raise ValueError('input file miss!!!') + if not attr.output_path: + raise ValueError('unspecified output path!!!') + + handle_task_name = scene_info.handle_task + if handle_task_name: + try: + handler_cls = get_class(f'scene_handler.{handle_task_name}', snake_to_camel(handle_task_name)) + scene_instance = handler_cls(attr) + + infer_thread = threading.Thread(target=scene_instance.run, args=()) + threads.append(infer_thread) + infer_thread.start() + + out_thread = threading.Thread(target=scene_instance.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + # logger.exception(e) + logger.exception(f'scene start failed: {e}') + else: + logger.warning(f'scene {scene_info.remark} for device {device_no} start failed beause no handle task') \ No newline at end of file diff --git a/server.py b/server.py index cc4cd49..529dca9 100644 --- a/server.py +++ b/server.py @@ -1,26 +1,98 @@ from http.server import BaseHTTPRequestHandler, HTTPServer +import json import os import subprocess +import time + +from constants import BOX_PRIVATE_KEY +from rsa_utils import decrypt_message_with_private_key +from urllib.parse import urlparse, parse_qs + +from global_logger import logger + +def check_secret(encrypt_message): + try: + message = decrypt_message_with_private_key(BOX_PRIVATE_KEY, encrypt_message) + command, timestamp = message.rsplit('_', 1) + current_time_ms = int(time.time() * 1000) + time_difference = current_time_ms - int(timestamp) + if time_difference <= 60 * 1000 * 5: + return command + else: + return 'timeout' + except Exception as e: + logger.exception(f"Failed to check secret: {e}") + return None + +def reboot(): + # 执行重启命令 + logger.info('trying to reboot') + try: + # 判断操作系统类型并执行相应的重启命令 + if os.name == "nt": # Windows 系统 + subprocess.run(["shutdown", "/r", "/t", "0"]) + else: # Linux / macOS + subprocess.run(["sudo", "reboot"]) + except Exception as e: + logger.exception(f"Failed to reboot: {e}") + +def sync_data(): + # todo + pass + # 定义HTTP请求处理器 class RequestHandler(BaseHTTPRequestHandler): - def do_GET(self): - # 检查路径是否为 /reboot - if self.path == "/reboot": - self.send_response(200) + def do_POST(self): + # 解析 URL 查询参数 + parsed_url = urlparse(self.path) + query_params = parse_qs(parsed_url.query) + + # 读取并解析请求体 JSON 数据 + content_length = int(self.headers.get('Content-Length', 0)) + post_data = self.rfile.read(content_length) + try: + if self.headers.get("Content-Type") == "application/json": + json_data = json.loads(post_data.decode('utf-8')) if post_data else {} + else: + json_data = {} + except json.JSONDecodeError: + self.send_response(400) self.send_header("Content-type", "application/json") self.end_headers() - self.wfile.write(b'{"message": "Rebooting system..."}') + self.wfile.write(b'{"message": "Invalid JSON format"}') + return + + # 检查路径是否为 /command + if parsed_url.path == "/command": + self.send_response(200) + self.send_header("Content-type", "application/json") - # 执行重启命令 - try: - # 判断操作系统类型并执行相应的重启命令 - if os.name == "nt": # Windows 系统 - subprocess.run(["shutdown", "/r", "/t", "0"]) - else: # Linux / macOS - subprocess.run(["sudo", "reboot"]) - except Exception as e: - print(f"Failed to reboot: {e}") + res_data = {} + secret = query_params.get("secret", [None])[0] + secret = json_data['secret'] + command = check_secret(secret) + logger.info(f'command={command}') + if not command: + res_data['code'] = 500 + res_data['message'] = 'Failed to decrypt' + elif command == 'timeout': + res_data['code'] = 500 + res_data['message'] = 'invalid timestamp' + else: + res_data['code'] = 200 + res_data['message'] = 'success' + + response_body = json.dumps(res_data).encode('utf-8') + self.send_header("Content-Length", str(len(response_body))) + self.end_headers() + self.wfile.write(response_body) + + if command == 'restart': + reboot() + if command == 'sync': + sync_data() + else: # 非 /reboot 请求返回 404 self.send_response(404) @@ -31,11 +103,11 @@ def run_server(host="0.0.0.0", port=5000): server_address = (host, port) httpd = HTTPServer(server_address, RequestHandler) - print(f"HTTP server running on {host}:{port}") + logger.info(f"HTTP server running on {host}:{port}") try: httpd.serve_forever() except KeyboardInterrupt: - print("\nServer is stopping...") + logger.error("\nServer is stopping...") httpd.server_close() if __name__ == "__main__": diff --git a/app.py b/app.py index 9bae452..0c89be9 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,13 @@ import base64 +import importlib from blockqueue import block_queue from callback_data_struct import * -from info_query import alarm_upload +from constants import ALARM_URI, SERVER_BASE_URL +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from http_tool import HttpTool import common import bufferpool +from global_logger import logger import math import queue @@ -26,6 +30,9 @@ import subprocess import argparse + +from string_utils import camel_to_snake, get_class, get_fun + # import ffmpeg try: import cv2 @@ -39,45 +46,8 @@ cancel_flag = multiprocessing.Value('b', False) +http_tool = HttpTool() -last_saved_time = {} -last_push_time = {} # key: output_url, value: time -last_alarm_time = {} # key: device_no, value: model_code-objcet_code: time - -class bbox(ctypes.Structure): - _fields_ = [ - ("xmin", ctypes.c_uint32), - ("ymin", ctypes.c_uint32), - ("xmax", ctypes.c_uint32), - ("ymax", ctypes.c_uint32), - ("score", ctypes.c_float), - ("id", ctypes.c_uint32), - ("label", ctypes.c_wchar * 64), - ("alarm", ctypes.c_uint8), - ("model_code", ctypes.c_wchar * 64), - ] - -class Box(ctypes.Structure): - _fields_ = [ - ("boxesnum", ctypes.c_uint32), - ("boxes", ctypes.ARRAY(bbox, 256)) - ] - -class SubClass(ctypes.Structure): - _fields_ = [ - ("model_code", ctypes.c_wchar * 64), - ("object_id",ctypes.c_uint32), - ("object_name", ctypes.c_wchar * 64), - ("conf_threshold", ctypes.c_float), - ("alarm_threshold", ctypes.c_uint32), - ("range", ctypes.ARRAY(ctypes.c_float, 8)) - ] - -class SubObject(ctypes.Structure): - _fields_ = [ - ("objectsnum", ctypes.c_uint32), - ("objects", ctypes.ARRAY(SubClass, 80)) - ] class ModelHandler: def __init__(self, model_path, plugin_path, model_name,model_code, handle_task, objects, image_width, image_height) -> None: @@ -85,9 +55,9 @@ self.plugin_path = plugin_path self.model_name = model_name self.model_code = model_code - self.handle_task = handle_task + self.handle_task = handle_task if handle_task else "base_model_handler" self.objects = objects - print(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') self.model, ret = sdk.lyn_load_model(self.model_path) common.error_check(ret, "lyn_load_model") @@ -145,18 +115,16 @@ sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold sub_obj_instance.objects[i].model_code = obj.model_code if obj.range: - # 'x1 y1 x2 y2 x3 y3 x4 y4' - range_values = [float(r) for r in obj.range] + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + # range_values = [float(r) for r in obj.range] abs_range = [] - for idx, value in enumerate(range_values): - if idx % 2 == 0: # x 坐标 (索引 0, 2, 4, 6) - abs_range.append(value * image_width) - else: # y 坐标 (索引 1, 3, 5, 7) - abs_range.append(value * image_height) - print(abs_range) + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) else: - sub_obj_instance.objects[i].range = (0, 0, 0, 0,0,0,0,0) + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) ret = sdk.lyn_memcpy( self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer @@ -167,54 +135,26 @@ self.apu_output_size * self.batch_size, 5 ) - print(f'batch_size = {self.batch_size}') - print(f'model_width = {self.model_width}') - print(f'model_height = {self.model_height}') - print(f'apu_output_size = {self.apu_output_size}') - print(f'class_num = {self.class_num}') - print(f'anchor_size = {self.anchor_size}') + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') -def recv_frame_cb(cb_data: cb_data): - # 放到ipe的处理队列中 - cb_data.block_queue.put(cb_data) - return 0 -def free_to_pool_callback(params): - vdec_out_pool = params[0] - data = params[1] - vdec_out_pool.push(data) - return 0 - - -def set_even(num): - num = math.floor(num) - if num % 2 != 0: - res = num - 1 - else: - res = num - return res - - -def set_padding_data(vidoe_width, video_height, model_width, model_height): - if vidoe_width > video_height: - resize_width = model_width - pad_x = 0 - resize_height = set_even(int(video_height * model_width / vidoe_width)) - pad_y = (model_height - resize_height) / 2 - else: - resize_height = model_height - pad_y = 0 - resize_width = set_even(int(vidoe_width * model_width / video_height)) - pad_x = (model_width - resize_width) / 2 - - return int(resize_width), int(resize_height), int(pad_x), int(pad_y) +def alarm_upload(data): + global http_tool + url = f'{SERVER_BASE_URL}{ALARM_URI}' + http_tool.post(url = url, data = data, need_token=True) def show_video_cb(params): cb_data: cb_data = params[0] model_infos = params[1] + last_alarm_time = params[2] device_no = cb_data.attr.device_no alarm_interval = cb_data.attr.alarm_interval @@ -239,6 +179,8 @@ c_box = ctypes.cast(host_buf_c, ctypes.POINTER(Box)).contents # box_dict = {"boxesnum": c_box.boxesnum, "boxes": []} # print(c_box.boxesnum) + + bbox_dicts = [] for i in range(c_box.boxesnum): bbox_instance = c_box.boxes[i] bbox_dict = { @@ -254,26 +196,11 @@ } # print(bbox_dict) + bbox_dicts.append(bbox_dict) - if bbox_dict["alarm"]: - last_time = last_alarm_time.get(device_no, {}).get(f'{model_code}_{bbox_dict["id"]}') - if last_time is None or (time.time() - last_time > alarm_interval): - if any(item["algoModelCode"] == bbox_dict["model_code"] - and item["recognitionTypeCode"] == bbox_dict["id"] - for item in alarm_infos): - continue - - alarm_info = { - "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), - "algoModelCode": bbox_dict["model_code"], - "deviceNo": device_no, - "recognitionTypeCode": bbox_dict["id"], - } - # 更新 last_alarm_time 中的时间 - if device_no not in last_alarm_time: - last_alarm_time[device_no] = {} # 初始化设备号的嵌套字典 - last_alarm_time[device_no][f'{model_code}_{bbox_dict["id"]}'] = time.time() - alarm_infos.append(alarm_info) + handle_task_name = model_info.handle_task + handle_alarm_info = get_fun(f'model_handler.{handle_task_name}', 'handle_alarm_info') + alarm_infos.extend(handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval)) if alarm_infos: data = np.empty(cb_data.frame.size, dtype=np.uint8) @@ -307,7 +234,7 @@ cv2.imwrite(file_path, rgbImg) for alarm_info in alarm_infos: - print(alarm_info) + logger.debug(alarm_info) alarm_info["picBase64"] = image_base64 alarm_upload(alarm_info) @@ -365,7 +292,7 @@ "model_code": bbox_instance.model_code, } box_dict["boxes"].append(bbox_dict) - print(box_dict) + logger.debug(box_dict) class yolov5: def __init__(self, attr: device_process_attr) -> None: @@ -398,7 +325,7 @@ common.error_check(ret, "lyn_demux_get_codec_para ") self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_demux_get_framerate") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') # 打开解码器 self.__vdec_attr.codec_id = self.codec_para.codec_id @@ -409,8 +336,8 @@ self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( self.codec_para, self.__vdec_attr ) - print(f'self.vdec_out_info.width = {self.vdec_out_info.width}') - print(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') common.error_check(ret, "lyn_vdec_get_out_info ") self.attr.width = self.vdec_out_info.width self.attr.height = self.vdec_out_info.height @@ -455,8 +382,9 @@ self.model_infos = [] self.model_nums = 0 for info in self.attr.model_configs: - self.model_infos.append(ModelHandler(f'{info.model_path}/Net_0', - f'{info.model_path}/plugin.so', + model_name = f'./models/{os.path.splitext(os.path.basename(info.model_path))[0]}' + self.model_infos.append(ModelHandler(f'{model_name}/Net_0', + f'{model_name}/plugin.so', info.model_name, info.model_code, info.handle_task, @@ -471,10 +399,10 @@ self.model_height = self.model_infos[0].model_height self.apu_output_size = self.model_infos[0].apu_output_size - print(f'self.batch_size = {self.batch_size}') - print(f'self.model_width = {self.model_width}') - print(f'self.model_height = {self.model_height}') - print(f'self.apu_output_size = {self.apu_output_size}') + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + logger.debug(f'self.apu_output_size = {self.apu_output_size}') self.ipe_output_size = ( self.model_width @@ -503,8 +431,8 @@ self.venc_recv_pool = bufferpool.buffer_pool( self.vdec_out_info.predict_buf_size, 5 ) - print(f'self.ipe_output_size = {self.ipe_output_size}') - print(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') # 设置编码参数 self.venc_attr = sdk.lyn_venc_attr_t() @@ -524,6 +452,7 @@ self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) self.start_process_time = None + self.last_push_time = None self.stream_out_process = None self.init_output_process() @@ -531,6 +460,8 @@ self.frame_step = 5 self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + def init_output_process(self): command = ['ffmpeg', @@ -544,11 +475,11 @@ try: self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) except OSError as e: - print(f"Failed to start process: {e}") + logger.exception(f"Failed to start process: {e}") except subprocess.SubprocessError as e: - print(f"Subprocess error: {e}") + logger.exception(f"Subprocess error: {e}") except Exception as e: - print(f"An unexpected error occurred: {e}") + logger.exception(f"An unexpected error occurred: {e}") # stream_out_process = ( # ffmpeg @@ -561,7 +492,7 @@ def read_stderr(process): for line in iter(process.stderr.readline, b''): - print(f"stderr: {line.decode('utf-8')}", end='') + logger.info(f"stderr: {line.decode('utf-8').strip()}") if self.stream_out_process: stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) @@ -572,11 +503,13 @@ def push_video(self): # show window global cancel_flag - global last_push_time + frame_rate = self.fps # 目标帧率 frame_interval = 1 / frame_rate # 每帧间隔时间 - last_push_time.setdefault(self.attr.output_path, time.time()) + + if not self.last_push_time: + self.last_push_time = time.time() while True: try: @@ -597,11 +530,11 @@ self.stream_out_process.stdin.write(image.tobytes()) # elapsed_time = time.time() - self.start_process_time - elapsed_time = time.time() - last_push_time[self.attr.output_path] + elapsed_time = time.time() - self.last_push_time sleep_time = frame_interval - elapsed_time - 0.01 # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') time.sleep(max(0, sleep_time)) - last_push_time[self.attr.output_path] = time.time() + self.last_push_time = time.time() def send_thread_func(self, cancel_flag): @@ -615,13 +548,13 @@ if ret != 0 and not eos: sdk.lyn_demux_close(self.__demux_hdl) time.sleep(500.0 / 1000) - print("demux failed, reconnecting...") + logger.warning("demux failed, reconnecting...") self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) if not ret: self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) common.error_check(ret, "lyn_synchronize_stream") - print(f'fps = {self.fps}') + logger.debug(f'fps = {self.fps}') continue # 发送给解码器解码 @@ -745,7 +678,7 @@ ret = sdk.lyn_stream_add_async_callback( self.model_infos[-1].plugin_stream, show_video_cb, - [cb_data, self.model_infos], + [cb_data, self.model_infos,self.last_alarm_time], ) common.error_check(ret, "lyn_stream_add_async_callback") @@ -1105,15 +1038,18 @@ if not attr.output_path: raise ValueError('unspecified output path!!!') - decoder = yolov5(attr) + try: + decoder = yolov5(attr) - infer_thread = threading.Thread(target=decoder.run, args=()) - threads.append(infer_thread) - infer_thread.start() + infer_thread = threading.Thread(target=decoder.run, args=()) + threads.append(infer_thread) + infer_thread.start() - out_thread = threading.Thread(target=decoder.push_video, args=()) - threads.append(out_thread) - out_thread.start() + out_thread = threading.Thread(target=decoder.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + logger.exception(f'model thread start failed: {e}') # decoder.close() diff --git a/callback_data_struct.py b/callback_data_struct.py index 8324b23..07c6dc3 100644 --- a/callback_data_struct.py +++ b/callback_data_struct.py @@ -86,6 +86,7 @@ self.height = 0 self.video_frame = 0 self.model_configs = None + self.scene_info = None self.device_no = "" self.alarm_interval = -1 diff --git a/common.py b/common.py index af664b8..0abfa4f 100644 --- a/common.py +++ b/common.py @@ -14,15 +14,17 @@ """ import os from datetime import datetime +import subprocess import threading import time +from global_logger import logger import pylynchipsdk as sdk def error_check(condition, log): if condition: - print("\n****** {} ERROR: {}".format(datetime.now(), log)) - os._exit(-1) + logger.error("\n****** {} ERROR: {}".format(datetime.now(), log)) + # os._exit(-1) ### @@ -30,28 +32,28 @@ ### def error_check_ex(condition, code_line, log): if condition: - print( + logger.error( "\n****** {} error_code_line: {} ERROR: {}".format( datetime.now(), code_line, log ) ) - os._exit(-1) + # os._exit(-1) def default_stream_error_handle(stream, errorMsg, params): - print("******* streamID : ", stream) - print("******* errorMsg.errCode : ", errorMsg.errCode) - print("******* errorMsg.errMsg : ", errorMsg.errMsg) - print("******* errorMsg.errModule : ", errorMsg.errModule) - print("******* errorMsg.errFunction : ", errorMsg.errFunction) - print("******* params : ", params) - os._exit(-1) + logger.error("******* streamID : ", stream) + logger.error("******* errorMsg.errCode : ", errorMsg.errCode) + logger.error("******* errorMsg.errMsg : ", errorMsg.errMsg) + logger.error("******* errorMsg.errModule : ", errorMsg.errModule) + logger.error("******* errorMsg.errFunction : ", errorMsg.errFunction) + logger.error("******* params : ", params) + # os._exit(-1) def check_device_id(device_id): device_num, ret = sdk.lyn_get_device_count() if device_id < 0 and device_id >= device_num: - print( + logger.error( "device_id {} invalid, should in range [0, {})".format( device_id, device_num ) @@ -72,7 +74,7 @@ current_second = current_time.second if last_second != current_second: current_time = time.strftime("%H:%M:%S", time.localtime()) - print(f"{current_time} fps: {fps} ") + logger.info(f"{current_time} fps: {fps} ") fps = 0 last_second = current_second fps += 1 @@ -83,11 +85,11 @@ def print_frame_rate(channel_name: str) -> None: record.fps = getattr(record, 'fps', 0) + 1 - current_second = datetime.now().second + current_second = time.time() record.last_second = getattr(record, 'last_second', current_second) - if record.last_second != current_second: - print(f'channel {channel_name}, fps {record.fps}') - record.last_second = current_second + if current_second - record.last_second >= 10: + logger.info(f'channel {channel_name}, fps {record.fps/10.0}') + record.last_second = time.time() record.fps = 0 @@ -103,3 +105,26 @@ file_name, file_extension = os.path.splitext(file_path) new_file_path = file_name + new_ext return new_file_path + +def get_box_sn(): + command = "mcutools -s" + try: + # 执行命令 + result = subprocess.run( + command, + shell=True, # 使用 shell 执行命令 + capture_output=True, # 捕获标准输出和错误 + text=True # 将输出作为文本而不是字节 + ) + # 检查返回码 + if result.returncode == 0: + r = result.stdout.strip() + r = r.replace("SN: ", "").strip() + return r + else: + logger.error('fail get box sn:') + logger.error(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') + return '' + except Exception as e: + logger.exception(f'fail get box sn: {e}') + return '' \ No newline at end of file diff --git a/constants.py b/constants.py new file mode 100644 index 0000000..1ace49d --- /dev/null +++ b/constants.py @@ -0,0 +1,14 @@ +SERVER_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCA63rwNnsJ4aPvLKVIYMm44/+kfPxsiRqKV+I9/kM6OWqxKACqGQpF9JYXX7kKYDi7I8D6oi11c6XrNrWkB1ZX4DfT4rO5xnV3wARz9cUO/1WEKLut68+AhlOAcBQbOOvDjsobY/AFbwURNx4SJOMRfNntId1eNy44XXjnpq8udQIDAQAB\n-----END PUBLIC KEY-----\n' +SERVER_PRIVATE_KEY = '' + +BOX_PUBLIC_KEY = '-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuS3W6uMVXhokIf7Pt+rPmOLlwuDFstTdPZ75ObpOizxsBDxQ+D0tqUscCEfpwqRsFQQmiwe7btMlWuyxz3g691DRPtst3OvBWArK1uOHsfsXI3EA0AgIMqxRMJkPcgYEtiyZYKbnNb/GFce7y5DY1gAyak7r9UiD9sv9epW5RxO3vOS8Gq/PqlreEsPFItL8NhUq8OC22P0ZDmvxbZ7KlX7X6hYufb3rIMJ/1zWQ9aHP/E6jUEDJ2Fit5HCYo7a3qV0EYkxx4//zexPE2GflFzoggbTGh2Y/HpR9vEAyejeosiwgXzrH3z0a0QC77smdGuQyraDOym0TN1i/8iAdlwIDAQAB\n-----END PUBLIC KEY-----\n' +BOX_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5Ldbq4xVeGiQh/s+36s+Y4uXC4MWy1N09nvk5uk6LPGwEPFD4PS2pSxwIR+nCpGwVBCaLB7tu0yVa7LHPeDr3UNE+2y3c68FYCsrW44ex+xcjcQDQCAgyrFEwmQ9yBgS2LJlgpuc1v8YVx7vLkNjWADJqTuv1SIP2y/16lblHE7e85Lwar8+qWt4Sw8Ui0vw2FSrw4LbY/RkOa/FtnsqVftfqFi59vesgwn/XNZD1oc/8TqNQQMnYWK3kcJijtrepXQRiTHHj//N7E8TYZ+UXOiCBtMaHZj8elH28QDJ6N6iyLCBfOsffPRrRALvuyZ0a5DKtoM7KbRM3WL/yIB2XAgMBAAECggEAGETOrtf1+/A3HhF0DamDbYkHDaRBpK9LE4FbLlqhYpQXn/J7thzmeBJ06QzrsOu1koeJuZzYzqxzb9gncYZTTAhWdANrnlFzdGNts/370bKTlLyBm8IBuQpmVZUptgrd4TY1H7qgAKZSgpY7oXEpSdqawN35BJttWBHh9Yfb0VmlyT79HX8rds6pyv18eZ84Oy7UPKafprgrKfBK4dmbcXcW8bHB0XEzUOggTqURk9548X3pezvPdVr+6M++xMiFW/+xx6W6SmHd5KffsKgWgxadc1b59MWABZAKX6rJaOj0nCkVkqKB0InqF15xDU/Pe7Q7RhEJ40qLcKfuOH31gQKBgQDyxyCcICdZjm7EECA6aLvyRNANsW02csGB2qkfwAX3PHpNJnHkkMgpZUH6WxBweP5t2GGknpZ6xPIYyiijcx1PslfNSG8Woo+cekZ9ywcjRN+ob+k09ke0XOZ/PwQZwgCs8FZaD31v+YlgML/oB+3hyOcprQ0Ju1qNgQr3mv2A1wKBgQDDQ6eeuiyhr3xfWS+VmkvrFOpnRUR1mXlSSdz2wIJIqQehLzgMq1PcMlLaWSRgCMCwFTxl0SGZlbJ1ojwMTZ8I/SihfyieC+7i1dRFhDCG1Mstd7AMnJxT4fUNbjU9Lkslc6Jl1tiK3QlmtwJA2TYjWy1SIt/Ges579bBxU0jxQQKBgQCkqY5iliRaR5K1BEL3msWu4iFCyE5MmbTJgCcpU7I4KBrDbQnefpxkBzuitFUIE+htVE+VucJSbnXNfn/lrFP7E/kdUL2X2pYRCZX6B9RFhZc0xQKlW0zy9feX4oPDfKO8qB3JY6wuYE9eoHHozEf51auDxs2LBjm6tAVePw8lbQKBgDmIf6UHGgtx12WIs7qSwfe8K+zNvgQgelzCoraZtkmOdE8LyKoC+SySL1NinJMnQQ8g0rD1U78bGoAKk0LUxB0z6nrbKq6WuYZglrD86AdyP4NgFqFAq9cTZs7UQB+MuluInLsA+MDBq/l1xtYwqgD05n+jAuhKWvivRZO+hGmBAoGBAK+NAcgWbQn+wKsAWb21oS6XkaI4hOax9vCmEE4Z+d5BBBgDPrHrEWh6BhzvPO6I3yXTbZ8agsJm2gNKgHma1hC4RpZgnnMfln91c5ZBfY4M7OD4p+7vSzTq26JQrex8+kfymBB/lYYhmUCyQzAWxsnus3CiV0biWJAR2T68H0oR\n-----END PRIVATE KEY-----\n' + +SERVER_BASE_URL = 'http://192.168.83.42:6909/safe-server' +LOGIN_URI = '/sync/device/login' +INFO_QUERY_URI = '/sync/device/deviceList' +HEART_URI = '/sync/device/heart' +ALARM_URI = '/record/upload' +FILE_MD5_URI = '/sync/device/md5' + +HEART_INTERVAL = 180 #心跳上传频率(秒) \ No newline at end of file diff --git a/detect_utils.py b/detect_utils.py new file mode 100644 index 0000000..c07e937 --- /dev/null +++ b/detect_utils.py @@ -0,0 +1,75 @@ +import ctypes +from ctypes import * +import math + +from callback_data_struct import cb_data + +class bbox(ctypes.Structure): + _fields_ = [ + ("xmin", ctypes.c_uint32), + ("ymin", ctypes.c_uint32), + ("xmax", ctypes.c_uint32), + ("ymax", ctypes.c_uint32), + ("score", ctypes.c_float), + ("id", ctypes.c_uint32), + ("label", ctypes.c_wchar * 64), + ("alarm", ctypes.c_uint8), + ("model_code", ctypes.c_wchar * 64), + ] + +class Box(ctypes.Structure): + _fields_ = [ + ("boxesnum", ctypes.c_uint32), + ("boxes", ctypes.ARRAY(bbox, 256)) + ] + +class SubClass(ctypes.Structure): + _fields_ = [ + ("model_code", ctypes.c_wchar * 64), + ("object_id",ctypes.c_uint32), + ("object_name", ctypes.c_wchar * 64), + ("conf_threshold", ctypes.c_float), + ("alarm_threshold", ctypes.c_uint32), + ("range", ctypes.ARRAY(ctypes.c_float, 8)) + ] + +class SubObject(ctypes.Structure): + _fields_ = [ + ("objectsnum", ctypes.c_uint32), + ("objects", ctypes.ARRAY(SubClass, 80)) + ] + +def recv_frame_cb(cb_data: cb_data): + # 放到ipe的处理队列中 + cb_data.block_queue.put(cb_data) + return 0 + + +def free_to_pool_callback(params): + vdec_out_pool = params[0] + data = params[1] + vdec_out_pool.push(data) + return 0 + + +def set_even(num): + num = math.floor(num) + if num % 2 != 0: + res = num - 1 + else: + res = num + return res + +def set_padding_data(vidoe_width, video_height, model_width, model_height): + if vidoe_width > video_height: + resize_width = model_width + pad_x = 0 + resize_height = set_even(int(video_height * model_width / vidoe_width)) + pad_y = (model_height - resize_height) / 2 + else: + resize_height = model_height + pad_y = 0 + resize_width = set_even(int(vidoe_width * model_width / video_height)) + pad_x = (model_width - resize_width) / 2 + + return int(resize_width), int(resize_height), int(pad_x), int(pad_y) \ No newline at end of file diff --git a/download_tool.py b/download_tool.py new file mode 100644 index 0000000..464d5b1 --- /dev/null +++ b/download_tool.py @@ -0,0 +1,176 @@ +from datetime import datetime +import hashlib +import os +import requests +import shutil +import zipfile + +from constants import FILE_MD5_URI, SERVER_BASE_URL +from global_logger import logger + +download_path = './downloads/' +weight_path = './models' +model_task_path = './model_handler' +scene_task_path = './scene_handler' + +def _file_url(file_path): + return f'{SERVER_BASE_URL}/static/{file_path}' + +def _file_md5_url(file_path): + return f'{SERVER_BASE_URL}/{FILE_MD5_URI}' + +def _calculate_md5(file_path, chunk_size=8192): + """高效计算文件的 MD5 值,使用分块读取""" + md5_hash = hashlib.md5() + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(chunk_size), b""): + md5_hash.update(chunk) + return md5_hash.hexdigest() + +def _download_file(file_path): + response = requests.get(_file_url(file_path), stream=True) + path = os.path.join(download_path,file_path) + os.makedirs(os.path.dirname(path), exist_ok=True) + + with open(path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + f.write(chunk) + return response + +def _get_server_md5(file_path): + params = {'path': file_path} + logger.debug(f'GET: {_file_md5_url(file_path)}, {params}') + response = requests.get(url=_file_md5_url(file_path),params=params) + if response.status_code == 200: + data = response.json() + if data['code'] == 200: + return data['data'] + else: + logger.error(f'get server md5 return error: {data}') + return None + else: + logger.error(f"get server md5 error: status_code: {response.status_code}, response: {response.text}") + return None + + +def _write_meta_file(meta_file, filename, version, size, md5): + with open(meta_file, 'w') as f: + f.write(f"filename: {filename}\n") + f.write(f"version: {version}\n") + f.write(f"size: {size}\n") + f.write(f"md5: {md5}\n") + f.write(f"last_modified: {datetime.now()}\n") + +# 读取 .meta 文件 +def _read_meta_file(meta_file): + if not os.path.exists(meta_file): + return None + meta_data = {} + with open(meta_file, 'r') as f: + for line in f: + # 处理空行或无效格式的行 + if not line.strip(): + continue + + # 确保行中至少包含冒号(防止解包错误) + parts = line.strip().split(": ", 1) + if len(parts) == 2: + key, value = parts + elif len(parts) == 1: + key, value = parts[0].replace(":","").strip(), "" # 处理 value 为空的情况 + else: + continue # 忽略错误格式的行 + + meta_data[key.strip()] = value.strip() + return meta_data + +def _is_file_up_to_date(local_path, meta_path, server_version, server_size, server_md5): + if not os.path.exists(local_path) or not os.path.exists(meta_path): + return False + + meta_data = _read_meta_file(meta_path) + logger.debug(f'meta_data={meta_data}') + if not meta_data: + return False + + # 检查文件版本、大小、MD5 + local_md5 = _calculate_md5(local_path) + + logger.debug(f'server: {server_version} {server_size} {server_md5}') + logger.debug(f'meta: {meta_data.get("version")} {meta_data.get("size")} {meta_data.get("md5")}') + return ( + meta_data.get("version") == server_version and + meta_data.get("size") == str(server_size) and + meta_data.get("md5") == server_md5 and + local_md5 == server_md5 #防止文件被篡改或意外更改 + ) + + + +def _extract_and_organize(zip_path, type): + folder_target = weight_path + py_target = model_task_path if type =='model' else scene_task_path + zip_name = os.path.splitext(os.path.basename(zip_path))[0] + logger.debug(f'zip_name={zip_name}, zip_path={zip_path}') + # 创建目标路径 + os.makedirs(folder_target, exist_ok=True) + os.makedirs(py_target, exist_ok=True) + + # 解压缩文件 + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + extract_dir = os.path.join(os.path.dirname(zip_path), "temp_extract") + zip_ref.extractall(extract_dir) + + # 遍历解压后的内容 + for root, dirs, files in os.walk(extract_dir): + for directory in dirs: + source_folder = os.path.join(root, directory) + target_folder = os.path.join(folder_target, zip_name) + logger.info(f"移动文件夹: {source_folder} 到 {target_folder}") + shutil.move(source_folder, target_folder) + + for file in files: + if file.endswith(".py"): + source_file = os.path.join(root, file) + target_file = os.path.join(py_target, file) + logger.info(f"移动 Python 文件: {source_file} 到 {target_file}") + shutil.move(source_file, target_file) + + # 清理临时解压目录 + shutil.rmtree(extract_dir) + logger.info("文件整理完成!") + +# 文件更新逻辑 +def check_file(file_path, file_version, type): + logger.info(f'checking file file_path={file_path} file_version={file_version}') + # filename = os.path.basename(file_path) + filename = file_path + local_path = os.path.join(download_path, filename) + meta_path = local_path + ".meta" + logger.debug(f'filename={filename}') + logger.debug(f'local_path={local_path}') + logger.debug(f'meta_path={meta_path}') + + # 获取服务器文件信息 + response = requests.head(_file_url(file_path)) + server_size = int(response.headers.get('Content-Length', 0)) + + # 如果本地文件已存在且是最新的,则跳过下载 + if os.path.exists(local_path): + logger.info(f'{local_path}已存在') + server_md5 = _get_server_md5(file_path) + if _is_file_up_to_date(local_path, meta_path, file_version, server_size, server_md5): + logger.info(f"{filename} 已经是最新版本,无需下载") + return + + # 下载新文件 + logger.info(f"正在下载 {filename}...") + _download_file(file_path) + + # 更新 .meta 文件 + local_md5 = _calculate_md5(local_path) + _write_meta_file(meta_path, filename, file_version, server_size, local_md5) + logger.info(f"{filename} 下载完成并更新元数据") + + # 解压文件,放到对应的位置 + _extract_and_organize(local_path, type) \ No newline at end of file diff --git a/global_logger.py b/global_logger.py new file mode 100644 index 0000000..47e41b7 --- /dev/null +++ b/global_logger.py @@ -0,0 +1,52 @@ +# logger.py +import logging.handlers +import os +import sys +from logging.handlers import TimedRotatingFileHandler + +# 确保日志目录存在 +log_dir = 'logs' +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# 实例化并导出全局日志记录器 +logger = logging.getLogger("casic_safe_logger") +logger.setLevel(logging.DEBUG) # 设置日志级别 + + +# 创建一个TimedRotatingFileHandler +handler = TimedRotatingFileHandler( + os.path.join(log_dir, 'app.log'), # 日志文件名 + when='midnight', # 每天午夜滚动 + interval=1 # 滚动间隔为1天 +) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +# 将handler添加到日志器 +logger.addHandler(handler) + +# 创建控制台处理器 +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) +console_handler.setFormatter(formatter) +logger.addHandler(console_handler) + + +# 将标准输出和标准错误重定向到日志记录器 +class StreamToLogger: + def __init__(self, logger, level): + self.logger = logger + self.level = level + + def write(self, message): + if message.strip(): # 忽略空消息 + self.logger.log(self.level, message) + + def flush(self): + pass # 这里不需要实现 + + +# 捕获所有stdout和stderr的内容到日志 +sys.stdout = StreamToLogger(logger, logging.INFO) +sys.stderr = StreamToLogger(logger, logging.ERROR) diff --git a/http_tool.py b/http_tool.py index d4bee99..a771b53 100644 --- a/http_tool.py +++ b/http_tool.py @@ -1,51 +1,234 @@ import requests import threading from typing import Dict, Any, Callable +import time +from global_logger import logger +from common import get_box_sn +from rsa_utils import encrypt_message_with_public_key +from constants import LOGIN_URI, SERVER_BASE_URL, SERVER_PUBLIC_KEY +import json +import copy + + +# 限制打印的长度 +def truncate_data(data, max_length=500): + data_copy = copy.deepcopy(data) + if isinstance(data_copy, dict): + return {k: truncate_data(v, max_length) for k, v in data_copy.items()} + elif isinstance(data_copy, list): + return [truncate_data(item, max_length) for item in data_copy] + elif isinstance(data_copy, str) and len(data_copy) > max_length: + return data_copy[:max_length] + "...(truncated)" + else: + return data_copy class HttpTool: - def __init__(self): - """初始化HTTP工具类""" - def post(self, url: str, data: Dict[str, Any] = None, headers: Dict[str, str] = None, + def __init__(self, box_sn=None): + """初始化HTTP工具类""" + self.box_sn = box_sn + self.server_token = None + self.token_ts = None + self.token_valid_period = 24 * 60 * 60 + + self._fetching_token = False + self._token_lock = threading.Lock() + + def _is_token_valid(self) -> bool: + """简单判断当前 token 是否还在有效期内""" + if self.server_token is None or self.token_ts is None: + return False + return (time.time() - self.token_ts) < self.token_valid_period + + def _get_token_params(self): + box_sn = self.box_sn or get_box_sn() + msg = f'{box_sn}_{int(time.time() * 1000)}' + logger.debug(f'token msg = {msg}') + secret = encrypt_message_with_public_key(SERVER_PUBLIC_KEY, msg) + url = f'{SERVER_BASE_URL}{LOGIN_URI}' + params = {'secret': secret, 'code':box_sn} + return url, params + + # --------------------------------------------------------- + # 1) 非阻塞的 fetch_token,完成后通过回调通知结果 + # --------------------------------------------------------- + def fetch_token_async( + self, + success_cb: Callable[[str], None], + fail_cb: Callable[[Exception], None] = None + ): + """ + 非阻塞获取 token:在子线程中拉取新的 token, + 获取成功后调用 success_cb(token_str);失败则调用 fail_cb(exception)。 + """ + def token_callback(response: requests.Response): + res = response.json() + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + # 成功回调 + if success_cb: + success_cb(new_token) + else: + # 失败 + if fail_cb: + fail_cb(Exception(f"Fetch token failed, response={res}")) + + + url, params = self._get_token_params() + t = threading.Thread(target=self.post, args=(url,params,None,None,False,token_callback), daemon=True) + t.start() + + # --------------------------------------------------------- + # 2) 真正发起POST请求的函数(子线程),完成后回调 + # --------------------------------------------------------- + + + + + def _do_post_request( + self, + url: str, + data: Dict[str, Any], + params: Dict[str, str], + headers: Dict[str, str], + callback: Callable[[requests.Response], None] + ): + """ + 在子线程中发起 HTTP POST 请求 + """ + try: + logger.info(f'POST: url={url} params={params} headers={headers} data={truncate_data(data)}') + response = requests.post(url, json=data, headers=headers, params=params) + if callback: + callback(response) + except Exception as e: + logger.exception(f"HTTP POST 请求失败: {e}") + if callback: + callback(None) + + # --------------------------------------------------------- + # 3) post:若 token 无效,先 fetch_token_async,然后在其成功回调里再发请求 + # --------------------------------------------------------- + def post(self, + url: str, + data: Dict[str, Any] = None, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True, callback: Callable[[requests.Response], None] = None): - + """ + 先检查 token,如果无效,就先异步获取 token,获取成功后再发起 HTTP 请求; + 获取/发送都在子线程,不会阻塞调用此 post 的主线程。 + """ if callback is None: callback = self.default_callback - """ - 异步POST请求 - :param url: 请求URL - :param data: POST的数据 - :param headers: 请求头 - :param callback: 回调函数,接收response对象 - """ - # 定义子线程的目标函数 - def _post_request(): - try: - response = requests.post(url, json=data, headers=headers) - if callback: - callback(response) - except Exception as e: - print(f"HTTP POST 请求失败: {e}") + + if not need_token: + # 不需要 token,直接发请求(异步) + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {},headers or {},callback), + daemon=True + ) + thread.start() + return + + # 需要 token 的情况 + # 1) 如果 token 有效,直接发请求 + if self._is_token_valid(): + # 构造带token的headers + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = self.server_token + + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {},params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + else: + # 2) token 无效,先异步获取 token,然后在成功回调里再发起请求 + def on_fetch_success(new_token: str): + # 再发起请求 + merged_headers = (headers or {}).copy() + merged_headers['Authorization'] = new_token + thread = threading.Thread( + target=self._do_post_request, + args=(url, data or {}, params or {}, merged_headers, callback), + daemon=True + ) + thread.start() + + def on_fetch_fail(e: Exception): + logger.error(f"Token 获取失败: {e}") + # 根据业务需求,可以在这里 callback(None) 或者返回错误等 if callback: callback(None) - # 创建并启动子线程 - thread = threading.Thread(target=_post_request) - thread.daemon = True # 设置守护线程 - thread.start() + self.fetch_token_async( + success_cb=on_fetch_success, + fail_cb=on_fetch_fail + ) + + + def get(self, + url: str, + params: Dict[str, str] = None, + headers: Dict[str, str] = None, + need_token: bool = True) -> requests.Response: + """ + Synchronous GET request with optional token handling. + """ + merged_headers = (headers or {}).copy() + if need_token: + if not self._is_token_valid(): + token_url, token_params = self._get_token_params() + logger.info(f'POST: url={token_url} json={token_params}') + token_response = requests.post(url=token_url, json=token_params) + if token_response is None: + logger.error('获取token失败') + return None + res = token_response.json() + logger.debug(res) + if res.get('code') == 200: + new_token = res['data'] + with self._token_lock: + self.server_token = new_token + self.token_ts = time.time() + merged_headers['Authorization'] = self.server_token + logger.info(f'GET: {url} {params} {merged_headers}') + response = requests.get(url, params=params, headers=merged_headers) + if response is not None: + return response.json() + else: + logger.error("请求失败或无响应") def default_callback(self, response: requests.Response): """ 默认回调函数 - :param response: HTTP响应对象 """ if response is not None: try: - print(f"状态码: {response.status_code}, 响应内容: {response.json()}") + logger.info(f"状态码: {response.status_code}, 响应内容: {response.json()}") except ValueError: - print(f"状态码: {response.status_code}, 响应内容: {response.text}") + logger.error(f"状态码: {response.status_code}, 响应内容: {response.text}") else: - print("请求失败或无响应") + logger.error("请求失败或无响应") +if __name__ == "__main__": + tool = HttpTool() + url = 'http://192.168.83.33:9001/test' + data = { + 'a': '1' + } + # tool.post(url=url,data=data,need_token=True) + + url = 'http://192.168.83.33:9001/list' + r = tool.get(url,need_token=True) + print(r) + time.sleep(5) \ No newline at end of file diff --git a/info_query copy.py b/info_query copy.py new file mode 100644 index 0000000..f6929a0 --- /dev/null +++ b/info_query copy.py @@ -0,0 +1,277 @@ + +from typing import Optional, get_type_hints + + +class BaseInfo: + # 定义字段映射,子类可重写 + field_mapping = {} + + def from_dict(self, data: dict): + """递归动态初始化对象""" + hints = get_type_hints(self.__init__) + print(f'h={hints}') + + for key, value in data.items(): + attr_key = self.field_mapping.get(key, key) + if hasattr(self, attr_key): + attr = getattr(self, attr_key) + print(f'attr={attr}, attr_key={attr_key}') + # 处理嵌套对象,如果为 None 则初始化 + if attr is None: + print('准备初始化嵌套对象...') + # 获取类型注解 + attr_type = hints.get(attr_key, None) + print(f'获取到的属性类型: {attr_type}') + # 处理 Optional[T] 类型 + if attr_type and hasattr(attr_type, '__origin__') and attr_type.__origin__ is Optional: + attr_type = attr_type.__args__[0] # 提取 Optional 内部类型 + + if attr_type and isinstance(attr_type, type) and issubclass(attr_type, BaseInfo): + print(f'初始化嵌套对象: {attr_key} -> {attr_type}') + attr = attr_type() # 初始化嵌套对象 + setattr(self, attr_key, attr) + + # 如果是嵌套对象,递归调用 from_dict + if isinstance(attr, BaseInfo): + print(f'1 {attr_key}') + attr.from_dict(value) + # 如果是列表并且需要初始化子对象 + elif isinstance(attr, list) and value and isinstance(value[0], dict): + print(f'2 {attr_key}') + obj_type = self._get_list_type(attr_key) + setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + else: + print(f'3 {attr_key}') + setattr(self, attr_key, value) + return self + # def from_dict(self, data: dict): + # for key, value in data.items(): + # attr_key = self.field_mapping.get(key, key) + + # if hasattr(self, attr_key): + # attr = getattr(self, attr_key) + + # # 处理嵌套对象 + # if isinstance(attr, BaseInfo) or (attr is None and attr_key in self.__annotations__): + # print(f'解析嵌套对象: {attr_key}') + # obj_type = self.__annotations__.get(attr_key) + # if attr is None and obj_type: + # attr = obj_type() + # attr.from_dict(value) + # setattr(self, attr_key, attr) + + # # 处理列表对象 + # elif isinstance(attr, list) and value and isinstance(value[0], dict): + # print(f'解析对象列表: {attr_key}') + # obj_type = self._get_list_type(attr_key) + # setattr(self, attr_key, [obj_type().from_dict(item) for item in value]) + + # # 直接赋值 + # else: + # print(f'直接赋值: {attr_key}') + # setattr(self, attr_key, value) + + # return self + + def _get_list_type(self, key): + """动态获取列表中子对象的类型""" + raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + def __str__(self): + """打印对象的所有属性和值""" + attributes = vars(self) + return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" + +class DeviceInfo(BaseInfo): + field_mapping = { + 'deviceId': 'device_id', + 'cameraIndexCode': 'device_no', + 'rtspUrl': 'input_url', + 'recognitionUrl': 'output_url', + 'recognitionInterval': 'recognition_interval', + 'alarmInterval': 'alarm_interval', + 'type': 'mode_type', + 'algoModels': 'model_relations', + 'sceneRelation': 'scene_relation', + } + + def __init__(self) -> None: + self.device_id = 0 + self.device_no = 0 + self.input_url = '' + self.output_url = '' + self.recognition_interval = -1 + self.alarm_interval = -1 + self.mode_type = -1 + # self.model_relations = [] + self.scene_relation : Optional[SceneInfo] = None + + def _get_list_type(self, key): + if key == "model_relations": + return ModelInfo + + +class ModelObject(BaseInfo): + field_mapping = { + 'algoModelId': 'model_id', + 'code': 'model_code', + 'typeCode': 'object_code', + 'remark': 'object_name', + 'isUse': 'is_use', + 'confidenceLevel': 'conf_threshold', + 'threshold': 'alarm_threshold', + 'boundary': 'range', + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.object_code = '' + self.object_name = '' + self.is_use = 0 + self.conf_threshold = 0.5 + self.alarm_threshold = 0 + self.range = None + +class ModelInfo(BaseInfo): + field_mapping = { + 'id': 'model_id', + 'code': 'model_code', + 'modelName': 'model_name', + 'path': 'model_path', + 'handleTask': 'handle_task', + 'remark': 'remark', + 'subObjects': 'objects', + 'version': 'version' + } + + def __init__(self) -> None: + self.model_id = 0 + self.model_code = '' + self.model_name = '' + self.model_path = '' + self.handle_task = '' + self.remark = '' + self.version = '' + self.objects = [] + + def _get_list_type(self, key): + if key == "objects": + return ModelObject + +class SceneInfo(BaseInfo): + field_mapping = { + 'sceneId': 'scene_id', + 'sceneCode': 'scene_code', + 'handleTask': 'handle_task', + 'boundary': 'boundary', + 'remark': 'remark', + 'version': 'version', + } + + def __init__(self) -> None: + self.scene_id = 0 + self.scene_code = '' + self.handle_task = '' + self.boundary = '' + self.remark = '' + self.version = '' + + +INFO_SOURCE = 'server' # server +HTTP_SERVER = 'http://192.168.83.42:6909' + +import json +import requests +from constants import INFO_QUERY_URI, SERVER_BASE_URL +from http_tool import HttpTool + + + +def query_device_info(box_sn): + if INFO_SOURCE == 'mock': + file_path = './mock/device.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + device_list = [] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} + try: + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + print(json.dumps(res_data, ensure_ascii=False)) + device_list = [] + data = res_data['data'] + for item in data: + device = DeviceInfo() + device.from_dict(item) + device_list.append(device) + return device_list + else: + print(f"fail to query device info") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + +def query_device_model(device_id): + if INFO_SOURCE == 'mock': + file_path = './mock/device_model.json' + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + model_list = [] + for item in data: + if item["device_id"] == device_id: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + except FileNotFoundError: + print(f"File not found: {file_path}") + return [] + except json.JSONDecodeError: + print("Error decoding JSON file.") + return [] + else: + url = HTTP_SERVER + f"/model/list?deviceId={device_id}" + try: + response = requests.get(url, timeout=3) + if response.status_code == 200: + data = response.json() + model_list = [] + for item in data: + model = ModelInfo() + model.from_dict(item) + model_list.append(model) + return model_list + else: + print(f"HTTP request failed with status code {response.status_code}") + return [] + except requests.RequestException as e: + print(f"HTTP request error: {e}") + return [] + + +if __name__ == "__main__": + device = DeviceInfo() + + # print(DeviceInfo.__annotations__) # 这里不会包含 scene_relation + print(device.__dict__) + hints = get_type_hints(DeviceInfo.__init__) + print(hints) + print(hints.get('scene_relation')) \ No newline at end of file diff --git a/info_query.py b/info_query.py index 2ae84a2..fd34fdb 100644 --- a/info_query.py +++ b/info_query.py @@ -1,167 +1,178 @@ -class BaseInfo: - def from_dict(self, data: dict): - """递归动态初始化对象""" - for key, value in data.items(): - if hasattr(self, key): - attr = getattr(self, key) - # 如果是嵌套对象,递归调用 from_dict - if isinstance(attr, BaseInfo): - attr.from_dict(value) - # 如果是列表并且需要初始化子对象 - elif isinstance(attr, list) and value and isinstance(value[0], dict): - obj_type = self._get_list_type(key) - setattr(self, key, [obj_type().from_dict(item) for item in value]) - else: - setattr(self, key, value) - return self +from dataclasses import MISSING, dataclass, field, fields +from typing import Any, Dict, List, Optional, Type, get_type_hints - def _get_list_type(self, key): - """动态获取列表中子对象的类型""" - raise NotImplementedError("必须在子类中实现 _get_list_type 方法") + + +class BaseInfo: + @classmethod + def from_dict(cls: Type['BaseInfo'], data: Dict[str, Any]) -> 'BaseInfo': + # obj_fields = {f.metadata.get('json_key', f.name): f.name for f in fields(cls)} + obj_fields = {f.metadata.get('json_key', f.name): f for f in fields(cls)} + init_data = {} + + # for json_key, attr_name in obj_fields.items(): + for json_key, field_info in obj_fields.items(): + + attr_name = field_info.name + field_type = field_info.type + default_value = ( + field_info.default + if field_info.default != MISSING + else field_info.default_factory() if field_info.default_factory != MISSING + else None # 这里设置一个全局的默认值 + ) + field_value = data.get(json_key, default_value) + + # if json_key in data: + try: + # field_value = data[json_key] + # field_type = next(f.type for f in fields(cls) if f.name == attr_name) + # 处理嵌套对象 + if isinstance(field_value, dict): + if hasattr(field_type, 'from_dict'): + init_data[attr_name] = field_type.from_dict(field_value) + else: + init_data[attr_name] = field_value + # 处理列表对象 + elif hasattr(field_type, '__origin__') and field_type.__origin__ == list: + if field_value: + if isinstance(field_value, str): # 仅当 field_value 是字符串时解析 + try: + field_value = json.loads(field_value) + except json.JSONDecodeError: + pass # 如果解析失败,则保留原始值 + list_item_type = field_type.__args__[0] + if hasattr(list_item_type, 'from_dict'): + init_data[attr_name] = [list_item_type.from_dict(item) if isinstance(item, dict) else item for item in field_value] + else: + init_data[attr_name] = field_value + else: + init_data[attr_name] = [] + + # 类型转换处理 + elif field_type == float: + init_data[attr_name] = float(field_value) if field_value not in (None, '') else default_value + elif field_type == int: + init_data[attr_name] = int(float(field_value)) if field_value not in (None, '') else default_value + elif field_type == bool: + init_data[attr_name] = field_value.lower() == 'true' if isinstance(field_value, str) else bool(field_value) + else: + init_data[attr_name] = field_value + except (ValueError, TypeError): + # 出现异常时回退到默认值 + init_data[attr_name] = default_value + + + return cls(**init_data) def __str__(self): """打印对象的所有属性和值""" attributes = vars(self) return f"{self.__class__.__name__}({', '.join(f'{k}={v}' for k, v in attributes.items())})" -class DeviceInfo(BaseInfo): - def __init__(self) -> None: - self.device_id = 0 - self.device_no = 0 - self.device_type = '' - self.input_url = '' - self.output_url = '' - self.recognition_interval = -1 - self.alarm_interval = -1 - self.mode_type = -1 +@dataclass +class BoundaryPoint(BaseInfo): + x: float + y: float -class ModelObject(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.object_code = '' - self.object_name = '' - self.conf_threshold = 0.5 - self.alarm_threshold = 0 - self.range = None - +@dataclass +class ModelObject(BaseInfo): + model_id: int = field(metadata={'json_key': 'algoModelId'}) + model_code : str = field(metadata={'json_key': 'code'}) + object_code : str = field(metadata={'json_key': 'recognitionTypeId'}) + object_name : str = field(metadata={'json_key': 'recognitionTypeName'}) + range : List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + is_use : int = field(metadata={'json_key': 'isUse'}, default=0) + conf_threshold : float = field(metadata={'json_key': 'confidenceLevel'}, default=0.5) + alarm_threshold : int = field(metadata={'json_key': 'threshold'}, default=0) + +@dataclass class ModelInfo(BaseInfo): - def __init__(self) -> None: - self.model_code = '' - self.model_name = '' - self.model_path = '' - self.handle_task = '' - self.objects = [] + model_id: int = field(metadata={'json_key': 'id'}) + model_code: str = field(metadata={'json_key': 'code'}) + model_name : str = field(metadata={'json_key': 'modelName'}) + model_path : str = field(metadata={'json_key': 'path'}) + handle_task : str = field(metadata={'json_key': 'handleTask'}) + remark : str = field(metadata={'json_key': 'remark'}) + version : str = field(metadata={'json_key': 'version'}) + objects : List[ModelObject] = field(metadata={'json_key': 'subObjects'}) def _get_list_type(self, key): if key == "objects": return ModelObject +@dataclass +class SceneInfo(BaseInfo): + scene_id: int = field(metadata={'json_key': 'sceneId'}) + scene_code: str = field(metadata={'json_key': 'sceneCode'}) + handle_task: str = field(metadata={'json_key': 'handleTask'}) + range: List[BoundaryPoint] = field(metadata={'json_key': 'boundary'}) + remark: str = field(metadata={'json_key': 'remark'}) + version: str = field(metadata={'json_key': 'version'}) + scene_path : str = field(metadata={'json_key': 'path'}) -INFO_SOURCE = 'mock' # server -HTTP_SERVER = 'http://192.168.83.42:6909' +@dataclass +class DeviceInfo(BaseInfo): + device_id: int = field(metadata={'json_key': 'deviceId'}) + device_no: str = field(metadata={'json_key': 'cameraIndexCode'}) + input_url: str = field(metadata={'json_key': 'rtspUrl'}) + output_url: str = field(metadata={'json_key': 'recognitionUrl'}) + + model_relations: List[ModelInfo] = field(metadata={'json_key': 'algoModels'}) + scene_relation : SceneInfo = field(metadata={'json_key': 'sceneRelation'}) + + recognition_interval: int = field(metadata={'json_key': 'recognitionInterval'}, default=-1) + alarm_interval: int = field(metadata={'json_key': 'alarmInterval'}, default=-1) + mode_type: int = field(metadata={'json_key': 'type'}, default=-1) + + +INFO_SOURCE = 'mock' # mock/server import json import requests -import subprocess +from constants import INFO_QUERY_URI, SERVER_BASE_URL from http_tool import HttpTool -def get_box_sn(): - command = "mcutools -s" - try: - # 执行命令 - result = subprocess.run( - command, - shell=True, # 使用 shell 执行命令 - capture_output=True, # 捕获标准输出和错误 - text=True # 将输出作为文本而不是字节 - ) - # 检查返回码 - if result.returncode == 0: - return result.stdout.strip() - else: - print(f' "error": {result.stderr.strip()}, "returncode": {result.returncode}') - return '' - except Exception as e: - print(e) - return '' +from global_logger import logger + + def query_device_info(box_sn): if INFO_SOURCE == 'mock': - file_path = './mock/device.json' + # file_path = './mock/device_server.json' + file_path = './mock/device_server_scene.json' try: with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) device_list = [] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list - except FileNotFoundError: - print(f"File not found: {file_path}") + except FileNotFoundError as e: + logger.warning(f"File not found: {file_path}") return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") + except json.JSONDecodeError as e: + logger.warning("Error decoding JSON file.") return [] else: - url = HTTP_SERVER + f"/device/list?box_code={box_sn}" + url = f"{SERVER_BASE_URL}{INFO_QUERY_URI}" + params = {'code': box_sn} try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() + http_tool = HttpTool(box_sn=box_sn) + res_data = http_tool.get(url, params=params, need_token=True) + if res_data: + logger.debug(json.dumps(res_data, ensure_ascii=False)) device_list = [] + data = res_data['data'] for item in data: - device = DeviceInfo() - device.from_dict(item) + device = DeviceInfo.from_dict(item) device_list.append(device) return device_list else: - print(f"HTTP request failed with status code {response.status_code}") + logger.error(f"fail to query device info") return [] except requests.RequestException as e: - print(f"HTTP request error: {e}") + logger.error(f"HTTP request error: {e}") return [] - -def query_device_model(device_id): - if INFO_SOURCE == 'mock': - file_path = './mock/device_model.json' - try: - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - model_list = [] - for item in data: - if item["device_id"] == device_id: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - except FileNotFoundError: - print(f"File not found: {file_path}") - return [] - except json.JSONDecodeError: - print("Error decoding JSON file.") - return [] - else: - url = HTTP_SERVER + f"/model/list?deviceId={device_id}" - try: - response = requests.get(url, timeout=3) - if response.status_code == 200: - data = response.json() - model_list = [] - for item in data: - model = ModelInfo() - model.from_dict(item) - model_list.append(model) - return model_list - else: - print(f"HTTP request failed with status code {response.status_code}") - return [] - except requests.RequestException as e: - print(f"HTTP request error: {e}") - return [] - -def alarm_upload(data): - http_tool = HttpTool() - url = HTTP_SERVER + '/safe-server/record/upload' - http_tool.post(url, data) \ No newline at end of file diff --git a/install/algo-run.service b/install/algo-run.service new file mode 100644 index 0000000..8d4aa57 --- /dev/null +++ b/install/algo-run.service @@ -0,0 +1,13 @@ +[Unit] +Description=Algo run Script +After=network.target + +[Service] +ExecStartPre=/bin/bash -c 'while ! docker ps | grep zlm; do sleep 1; done' +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/main.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/algo-server.service b/install/algo-server.service new file mode 100644 index 0000000..8a3c39f --- /dev/null +++ b/install/algo-server.service @@ -0,0 +1,12 @@ +[Unit] +Description=Algo server Script +After=network.target + +[Service] +ExecStart=/usr/bin/python3 /home/lynxi/casic/code/casic_demo/server.py +Restart=always +RestartSec=5 +WorkingDirectory=/home/lynxi/casic/code/casic_demo + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/install/readme.md b/install/readme.md new file mode 100644 index 0000000..8ae2bb4 --- /dev/null +++ b/install/readme.md @@ -0,0 +1,22 @@ +cd ~/casic/tools/lyn-offline/pip +dpkg -i python3-lib2to3_3.8.10-0ubuntu1~20.04_all.deb +dpkg -i python3-distutils_3.8.10-0ubuntu1~20.04_all.deb +python3 get-pip.py --no-index --find-links . --verbose +pip3 install numpy-1.24.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install opencv_python-4.8.0.76-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl +pip3 install /usr/local/lynxi/sdk/pysdk/whl/pylynchipsdk-1.17.1-py38-none-linux_aarch64.whl + +cd ~/casic/tools +docker load -i zlmediskit-arm64.tar +docker run -id -p 1935:1935 -p 8080:80 -p 8443:443 -p 8554:554 -p 10000:10000 -p 10000:10000/udp -p 8000:8000/udp -p 9000:9000/udp --restart=always --name=zlm zlmediakit/zlmediakit:master + +cd /home/lynxi/casic/code/casic_demo/install +cp algo-run.service /etc/systemd/system/ +cp algo-server.service /etc/systemd/system/ +cd /etc/systemd/system/ +chmod +x algo-run.service +chmod +x algo-server.service +sudo systemctl enable algo-server.service +sudo systemctl start algo-server.service +sudo systemctl enable algo-run.service +sudo systemctl start algo-run.service \ No newline at end of file diff --git a/main.py b/main.py index 2ba27bf..da8bafd 100644 --- a/main.py +++ b/main.py @@ -1,39 +1,70 @@ +import threading +import time import pylynchipsdk as sdk -from info_query import get_box_sn,query_device_info,query_device_model +from common import get_box_sn +from constants import HEART_INTERVAL, HEART_URI, SERVER_BASE_URL +from download_tool import check_file +from global_logger import logger +from http_tool import HttpTool +from info_query import query_device_info from app import run_device +from scene_runner import run_scene_device + +def upload_heart(box_sn): + # todo 细化每个摄像头的推理状态? + if not box_sn: + logger.error('box_sn is empty, exit upload heart') + return + tool = HttpTool() + while True: + try: + url = f'{SERVER_BASE_URL}{HEART_URI}' + params = {'code': box_sn} + tool.get(url=url,params=params,need_token=True) + except Exception as e: + logger.exception(f'upload heart failed: {e}') + time.sleep(HEART_INTERVAL) + if __name__ == "__main__": box_sn = get_box_sn() - if not box_sn: - print('fail get box sn') - else: - print(f"box_sn = {box_sn}") - + logger.info(f"box_sn = {box_sn}") lyn_device_count, ret = sdk.lyn_get_device_count() if ret != 0 or lyn_device_count <= 0: - print("Error: Unable to fetch device count or no devices available.") + logger.error("Error: Unable to fetch device count or no devices available.") exit(1) - device_list = query_device_info(box_sn) - threads = [] + device_idx = 0 for idx, device in enumerate(device_list): - print(device) - if device.mode_type == '0': - model_list = query_device_model(device.device_id) + logger.debug(device) + if device.mode_type == 0: + model_list = device.model_relations for model in model_list: - print(model) - + logger.debug(model) + check_file(file_path=model.model_path,file_version=model.version,type='model') if model_list: - lyn_device_id = idx % lyn_device_count + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================model {len(model_list)}') run_device(device.device_no, device.input_url,device.output_url,lyn_device_id, model_list, device.alarm_interval, threads) - elif device.mode_type == '1': + elif device.mode_type == 1: # todo + scene_info = device.scene_relation + if scene_info: + lyn_device_id = (device_idx) % lyn_device_count + device_idx += 1 + logger.debug(f'===========================================scene {scene_info}') + # check_file(file_path=scene_info.scene_path,file_version=scene_info.version ,type='scene') + run_scene_device(device.device_no, device.input_url,device.output_url,lyn_device_id, scene_info, device.alarm_interval, threads) pass + heart_thread = threading.Thread(target=upload_heart, args=(box_sn,)) + heart_thread.start() + threads.append(heart_thread) # 等待所有线程完成 for thread in threads: diff --git a/mock/device_model.json b/mock/device_model.json index 0af817b..e5d3ebd 100644 --- a/mock/device_model.json +++ b/mock/device_model.json @@ -4,7 +4,7 @@ "model_code": "003", "model_name": "yolov5", "model_path": "models/safe-v5-20241121", - "handle_task": "", + "handle_task": "base_model_handler", "objects": [ { "model_code": "003", diff --git a/mock/device_server.json b/mock/device_server.json new file mode 100644 index 0000000..30d5865 --- /dev/null +++ b/mock/device_server.json @@ -0,0 +1,91 @@ +[ + + { + "alarmInterval": 60, + "algoModels": [ + { + "code": "001", + "createTime": "", + "handleTask": "base_model_handler", + "id": "1854890172563443714", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypes": "", + "remark": "", + "status": "", + "statusName": "", + "subObjects": [ + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171422507009", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "0", + "recognitionTypeName": "人", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + }, + { + "algoModelId": "1854890172563443714", + "boundary": "", + "code": "001", + "confidenceLevel": "0.5", + "createTime": "2024-12-03", + "deviceId": "1805427685028392962", + "handleTask": "base_model_handler", + "id": "1863885171426701315", + "isUse": "1", + "modelName": "行为识别模型", + "path": "2025-01/cf31dd14a8494beaa7644f1a1c8d2a41.zip", + "recognitionTypeId": "1", + "recognitionTypeName": "头", + "remark": "", + "threshold": "0", + "typeCode": "003", + "updateTime": "2024-12-03" + } + ], + "updateTime": "", + "version": "" + } + ], + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": "", + "type": 0, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/mock/device_server_scene.json b/mock/device_server_scene.json new file mode 100644 index 0000000..0b1ccf8 --- /dev/null +++ b/mock/device_server_scene.json @@ -0,0 +1,49 @@ +[ + + { + "alarmInterval": 60, + "algoModels": "", + "boundary": "", + "boxId": "1", + "boxName": "203灵汐盒子", + "cameraIndexCode": "34020000001320000007", + "code": "EMBLA23C29A00115", + "createTime": "2024-12-03", + "deptId": "1780179168070664194", + "deptName": "测试组织", + "deviceId": "1805427685028392962", + "deviceModelRelations": [ + { + "$ref": "$.data[2].algoModels[0].subObjects[0]" + }, + { + "$ref": "$.data[2].algoModels[0].subObjects[1]" + } + ], + "deviceName": "IPCamera 07", + "deviceNo": "34020000001320000007", + "deviceStatus": "0", + "deviceStatusName": "离线", + "id": "1863885171405729793", + "nvrIndexCode": "34020000001110000002", + "recognitionInterval": 60, + "recognitionUrl": "rtsp://192.168.83.100:8554/live/34020000001320000007", + "rtspUrl": "rtsp://admin:Casic203@192.168.83.64:554/Streaming/Channels/101", + "sceneRelation": { + "boundary": "[{\"x\":\"0.0651\",\"y\":\"0.6771\"},{\"x\":\"0.1948\",\"y\":\"0.6799\"},{\"x\":\"0.1932\",\"y\":\"0.7938\"},{\"x\":\"0.0666\",\"y\":\"0.8049\"}]", + "createTime": "", + "deviceId": "1788491058556604418", + "handleTask": "base_scene_handler", + "id": "1882003377202700289", + "path": "", + "remark": "闸井施工安全1", + "sceneCode": "002", + "sceneId": "1881511366112927745", + "sceneName": "闸井施工安全1", + "updateTime": "2025-01-22", + "version": "v0.0.2" + }, + "type": 1, + "updateTime": "2024-12-03" + } +] \ No newline at end of file diff --git a/model_handler/base_model_handler.py b/model_handler/base_model_handler.py new file mode 100644 index 0000000..dff16fc --- /dev/null +++ b/model_handler/base_model_handler.py @@ -0,0 +1,24 @@ +import time + +def handle_alarm_info(bbox_dicts, last_alarm_time,device_no, alarm_interval): + alarm_infos = [] + for bbox_dict in bbox_dicts: + if bbox_dict["alarm"] != 0: + model_code=bbox_dict["model_code"] + last_time = last_alarm_time.get(f'{model_code}_{bbox_dict["id"]}') + if last_time is None or (time.time() - last_time > alarm_interval): + if any(item["algoModelCode"] == bbox_dict["model_code"] + and item["recognitionTypeCode"] == bbox_dict["id"] + for item in alarm_infos): + continue + + alarm_info = { + "alarmTime": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), + "algoModelCode": bbox_dict["model_code"], + "deviceNo": device_no, + "recognitionTypeCode": bbox_dict["id"] if bbox_dict["alarm"]==1 else bbox_dict["alarm"], + } + # 更新 last_alarm_time 中的时间 + last_alarm_time[f'{model_code}_{bbox_dict["id"]}'] = time.time() + alarm_infos.append(alarm_info) + return alarm_infos diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log new file mode 100644 index 0000000..f594cfd --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/APU.log @@ -0,0 +1,70 @@ +APU log : model-safe-v5-20241121/Net_0/apu_0/APU.log +APU cmd : /home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 +Git version : Lynabc_hw3_20240112_1.17.0_0004 +APU_LOG_LEVEL: 1 +inGraph : false + +Options: + input = model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin + shape = {1,640,640,3} + dtype = uint8 + input file = + output = ./ + isGen = true + isGolden = false + mode = 0 (name: 0) + type = op + nPresetCores = 0 + isProfiler = false + isRefSvg = 0 + isSliceInfo = false + isClearFilterOut= true + batch = 1 (forcebatch: false) + batchOutputArrange= false + forcecomposite = false + ctrlopMoreCmdSize= -1 + inputprocmode = -1 + serialize = true + isPrimLog = false + strategyMode = -1 + binLoadMode = 0 + broadEnable = true + optFlag = 0 (0x0) + strategyOptFlag= 0 (0x0) + bEnableGlbDDRSave= 0 + ddrMemMode = 0 + broadRxChannel = -1 + forceSyn = 0 + dataCheck = false + enableMoveToDDR= true + scaleMaxEn = 0 + rangeProtect = -1 + infProtect = -1 + sliceStrategy = -1 + maxRetryCount = -1 + paddingMode = 0 + +num of Op nodes: + total[460] const[140] if[0] else[0] endif[0] do[0] while[0] load[0] save[0] multiinput[0] accop[0] userop[0](armop[0]) + loadsize[0.00] loadsizeOnGlb[0.00] + savesize[0.00] savesizeOnGlb[0.00] + +[2025-01-13 15:48:58:914 /apu_share/auto_build/peixu.wang/APU_src/ABC_release/LynABC/temp/LynABC/src/map/map.cpp:112]lx_map is calling. lx_map compile option: + git tag : 0 + APU_LOG_LEVEL: 1 + isNewCmd : true + gen_golden : false + savePDF : false + release : false + logFile : "model-safe-v5-20241121/Net_0/apu_0/APU.log" + batch : 1 + isUseSliceLib: true + + +MC conv info: + nMCConv : 64 + nConvOpMerged: 56 (enable: true) + nFastConv : 1 (enable: true) +lx_map ret 0 - success +lynabc_v0 ret 0 - success +COMPILER: build result is PASS diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt new file mode 100644 index 0000000..43e72a6 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/abc_cmd.txt @@ -0,0 +1 @@ +/home/ubuntu/anaconda3/envs/lynxi/lib/python3.6/site-packages/tvm/GenABC -i model-safe-v5-20241121/Net_0/apu_0/apu_lib.bin -s 1,640,640,3 -o ./ -v -1 -r false -g true -e false -x 0 -m 0 -d uint8 -b 1 -c 0 --optFlag 0 --inputprocmode -1 --ctrlopMoreCmdSize -1 --scaleMaxEn 0 diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin new file mode 100644 index 0000000..658f3ed --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_lib.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json new file mode 100644 index 0000000..204f99f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/apu.json @@ -0,0 +1,85 @@ +{ + "apu_x": { + "batch_size": 1, + "cmd_size": 512, + "ddr_config_size": 2608, + "image_size": 2484640, + "inputs": { + "datai0": { + "dtype": "uint8", + "load_data_mode": 0, + "lookup_addr_loop": 0, + "lookup_addr_name": "", + "lookup_addr_size": 0, + "lookup_base_addr": 0, + "name": "input", + "shape": "[1, 640, 640, 3]", + "shape_ex": [ + 1, + 640, + 640, + 3 + ], + "size": 1228800 + } + }, + "n_slice": 1, + "outputs": { + "datao0": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output0", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output0", + "shape": "[1, 3, 6400, 10]", + "shape_ex": [ + 1, + 3, + 6400, + 10 + ], + "size": 192000 + }, + "datao1": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output1", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output1", + "shape": "[1, 3, 1600, 10]", + "shape_ex": [ + 1, + 3, + 1600, + 10 + ], + "size": 48000 + }, + "datao2": { + "dtype": "float16", + "load_data_mode": 0, + "lookup_addr_loop": 32, + "lookup_addr_name": "lookup_output2", + "lookup_addr_size": 8, + "lookup_base_addr": 37580963840, + "name": "output2", + "shape": "[1, 3, 400, 10]", + "shape_ex": [ + 1, + 3, + 400, + 10 + ], + "size": 12000 + } + }, + "p2p_direct": "false", + "sub_image_size": "[2484640]", + "sub_image_size_ex": 2484640, + "total_calc": 20073932000.0 + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin new file mode 100644 index 0000000..0b79fca --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin new file mode 100644 index 0000000..59239c0 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/core.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin new file mode 100644 index 0000000..e0c5cf1 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/ddr_lut.bin diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin new file mode 100644 index 0000000..910ebd5 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lookup_ddr_addr.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl new file mode 100644 index 0000000..13775db --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/lyn__2025-01-13-15-48-59-982962.mdl Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin new file mode 100644 index 0000000..49689ea --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/pi_ddr_config.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json new file mode 100644 index 0000000..cdf449f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/snn.json @@ -0,0 +1,17 @@ +{ + "Reset": { + "addrBase": 38281412608, + "membranceDat": "./ddr.dat", + "membranceSet": null, + "size": 0, + "snnLookUpAddr": 0 + }, + "description": "this is weight information and membrance information", + "unReset": { + "addrBase": 38386270208, + "size": 0, + "snnLookUpAddr": 0, + "wightDat": "./unReset.dat", + "wightSet": null + } +} diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin new file mode 100644 index 0000000..72296bb --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/apu_x/super_cmd.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/case0/net0/chip0/tv_mem/data/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin new file mode 100644 index 0000000..656ae3b --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/dat.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat new file mode 100644 index 0000000..643eaa8 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/input.dat Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/data/100/output_ddr_2.dat diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log new file mode 100644 index 0000000..2991779 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/fpga_config.log @@ -0,0 +1,32 @@ +core_num = 30 +chip_num = 1 +phase_num = 283 +pause_phase = 283 +phase_num4 = 168 +phase_loop = 1 +svn_ver = Lynabc_hw3_20240112_1.17.0_0004 +inputs = [(5,0), ] +outputs = [(5,282), ] +input_buf_addr = 0x9b6000000 +input_size = 1228800 +output_buf_addr = 0x9b5ffff80 +output_size = 4 +batch_size = 1 +base_addr = 0x9be000000 +datachain_addr = 0x900000000 +run_mode = 2 +lookup_mbatch = 64 +perf_en = 0 +wdat_en = 0 + +ddr_in_phase1 = -1 +ddr_in_addr1 = 0x8c0000000 +ddr_in_file1 = ../../../../../apu_x/lookup_ddr_addr.bin +ddr_in_len1 = 512 + +ddr_out_phase1 = 282 +ddr_out_addr1 = 0x9c0000000 +ddr_out_file1 = output_ddr.dat +ddr_out_len1 = 504000 + +built_time = "2025/01/13 15:48:59" diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin new file mode 100644 index 0000000..6494e33 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/apu_0/prim_graph.bin Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json new file mode 100644 index 0000000..38afa37 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/Net_0/top_graph.json @@ -0,0 +1,51 @@ +{ + "main": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_concat_output": false + }, + "apu_0": { + "inputs": { + "input": { + "shape": "(1, 640, 640, 3)", + "dtype": "uint8", + "batch": 1 + } + }, + "outputs": { + "apu_0:0": { + "shape": "(1, 3, 6400, 10)", + "dtype": "float16" + }, + "apu_0:1": { + "shape": "(1, 3, 1600, 10)", + "dtype": "float16" + }, + "apu_0:2": { + "shape": "(1, 3, 400, 10)", + "dtype": "float16" + } + }, + "is_abc": "true", + "version": "2024010009" + } +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json new file mode 100644 index 0000000..cb8ff93 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/net_params.json @@ -0,0 +1,21 @@ +{ + "version": "2024010009", + "target": "apu", + "is_map": true, + "chip_num": 1, + "net_num": 1, + "parallel_mode": "DP", + "run_batch": 1, + "cpu_arch": "arm", + "cc": "aarch64-linux-gnu-g++", + "file_name": { + "cpu_lib": "deploy_lib.so", + "cpu_graph": "deploy_graph.json", + "cpu_params": "deploy_param.params", + "apu_lib": "apu_lib.bin", + "io_config": "top_graph.json" + }, + "net:": null, + "is_dynamic_shape": false, + "build_mode": "abc" +} \ No newline at end of file diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so new file mode 100644 index 0000000..e066361 --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so Binary files differ diff --git a/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 new file mode 100644 index 0000000..fa9aa0f --- /dev/null +++ b/models/cf31dd14a8494beaa7644f1a1c8d2a41/plugin.so.1 Binary files differ diff --git a/rsa_utils.py b/rsa_utils.py new file mode 100644 index 0000000..a25fa2f --- /dev/null +++ b/rsa_utils.py @@ -0,0 +1,49 @@ +import base64 +from cryptography.hazmat.primitives import serialization, hashes +from cryptography.hazmat.primitives.asymmetric import rsa, padding +from cryptography.hazmat.backends import default_backend + + +def encrypt_message_with_public_key(public_key_pem: str, message: str) -> str: + """ + 用公钥(字符串形式)对明文进行加密,并返回 base64 编码后的密文字符串 + """ + # 1. 从公钥字符串还原为公钥对象 + public_key = serialization.load_pem_public_key( + public_key_pem.encode("utf-8"), # 字符串转 bytes + backend=default_backend() + ) + + # 2. 对明文进行加密(OAEP 填充 + SHA256),返回二进制密文 + ciphertext_bytes = public_key.encrypt( + message.encode("utf-8"), + padding.PKCS1v15() + ) + + # 3. 将二进制密文转成 base64 字符串,方便通过 HTTP 或其他渠道传输 + ciphertext_base64 = base64.b64encode(ciphertext_bytes).decode("utf-8") + + return ciphertext_base64 + + +def decrypt_message_with_private_key(private_key_pem: str, ciphertext_base64: str) -> str: + """ + 用私钥(字符串形式)对 base64格式的密文进行解密,返回明文字符串 + """ + # 1. 从私钥字符串还原为私钥对象 + private_key = serialization.load_pem_private_key( + private_key_pem.encode("utf-8"), # 字符串转 bytes + password=None, # 如果在生成私钥时使用了加密算法,这里需要提供密码 + backend=default_backend() + ) + + # 2. Base64解码出二进制密文 + ciphertext_bytes = base64.b64decode(ciphertext_base64) + + # 3. 使用私钥解密 + plaintext_bytes = private_key.decrypt( + ciphertext_bytes, + padding.PKCS1v15() + ) + + return plaintext_bytes.decode("utf-8") \ No newline at end of file diff --git a/scene_handler/base_scene_handler.py b/scene_handler/base_scene_handler.py new file mode 100644 index 0000000..83fc593 --- /dev/null +++ b/scene_handler/base_scene_handler.py @@ -0,0 +1,722 @@ +import struct +import numpy as np +import pylynchipsdk as sdk +import copy +import sys, os +import ctypes +from ctypes import * +import subprocess +import threading +import multiprocessing +import time + +from global_logger import logger +from blockqueue import block_queue +from callback_data_struct import * +import common +import bufferpool +from detect_utils import bbox, Box, SubObject, SubClass, free_to_pool_callback, recv_frame_cb, set_padding_data +from info_query import ModelObject + +def save_file_cb(params): + cb_data: save_file_cb_data = params[0] + frame_cb_data: cb_data = params[1] + + packet = cb_data.packet + data_size, ret = sdk.lyn_enc_get_remote_packet_valid_size(packet) + common.error_check(ret, "lyn_enc_get_remote_packet_valid_size") + data = np.zeros(data_size, dtype=np.byte) + data_ptr = sdk.lyn_numpy_to_ptr(data) + ret = sdk.lyn_memcpy( + data_ptr, packet.data, data_size, sdk.lyn_memcpy_dir_t.ServerToClient + ) + common.error_check(ret, "lyn_memcpy") + + cb_data.video_frame.put([data, packet.eos]) + cb_data.recv_pool.push(packet.data) + return ret + +class SceneModelHandler: + def __init__(self, model_path, plugin_path, model_name,model_code, objects, image_width, image_height) -> None: + self.model_path = model_path + self.plugin_path = plugin_path + self.model_name = model_name + self.model_code = model_code + self.objects = objects + logger.info(f'init model: model_path = {self.model_path}, plugin_path = {self.plugin_path}, model_name = {self.model_name}') + + self.model, ret = sdk.lyn_load_model(self.model_path) + common.error_check(ret, "lyn_load_model") + + self.plugin, ret = sdk.lyn_plugin_register(self.plugin_path) + common.error_check(ret, "lyn_plugin_register") + + self.apu_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.plugin_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.apu_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.model_desc, ret = sdk.lyn_model_get_desc(self.model) + common.error_check(ret, "lyn_model_get_desc") + self.batch_size = self.model_desc.inputTensorAttrArray[0].batchSize + self.model_width = self.model_desc.inputTensorAttrArray[0].dims[2] + self.model_height = self.model_desc.inputTensorAttrArray[0].dims[1] + + self.apu_output_size = self.model_desc.outputDataLen + + self.class_num = self.model_desc.outputTensorAttrArray[0].dims[3] - 5 + self.anchor_size = self.model_desc.outputTensorAttrArray[0].dims[1] + self.boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + + self.sub_objects_info, ret = sdk.lyn_malloc(ctypes.sizeof(SubObject)) + + sub_obj = sdk.c_malloc(ctypes.sizeof(SubObject)) + + # 获取底层指针 + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + pythonapi.PyCapsule_GetPointer.restype = c_void_p + raw_ptr = ctypes.pythonapi.PyCapsule_GetPointer(sub_obj, None) + if not raw_ptr: + raise ValueError("Failed to extract pointer from PyCapsule") + + # 转换为 ctypes.POINTER(SubObject) + sub_obj_ptr = ctypes.cast(raw_ptr, ctypes.POINTER(SubObject)) + + # 通过 ctypes.POINTER 操作数据 + sub_obj_instance = sub_obj_ptr.contents + sub_obj_instance.objectsnum = len(self.objects) + + for i in range(sub_obj_instance.objectsnum): + obj = self.objects[i] + sub_obj_instance.objects[i].object_id = int(obj.object_code) + sub_obj_instance.objects[i].object_name = obj.object_name + sub_obj_instance.objects[i].conf_threshold = obj.conf_threshold + sub_obj_instance.objects[i].alarm_threshold = obj.alarm_threshold + sub_obj_instance.objects[i].model_code = obj.model_code + if obj.range: + # 转为(x1 y1 x2 y2 x3 y3 x4 y4) + abs_range = [] + for boundary_point in obj.range: + abs_range.append(boundary_point.x * image_width) + abs_range.append(boundary_point.y * image_height) + logger.debug(f'abs_range = {abs_range}') + sub_obj_instance.objects[i].range = (c_float * 8)(*abs_range) + else: + sub_obj_instance.objects[i].range = (0, 0, 0, 0, 0, 0, 0, 0) + + ret = sdk.lyn_memcpy( + self.sub_objects_info, sub_obj, ctypes.sizeof(SubObject), sdk.lyn_memcpy_dir_t.ClientToServer + ) + common.error_check(ret, "save_boxinfo_cb lyn_memcpy") + + self.apu_output_mem_pool = bufferpool.buffer_pool( + self.apu_output_size * self.batch_size, 5 + ) + + logger.debug(f'batch_size = {self.batch_size}') + logger.debug(f'model_width = {self.model_width}') + logger.debug(f'model_height = {self.model_height}') + logger.debug(f'apu_output_size = {self.apu_output_size}') + logger.debug(f'class_num = {self.class_num}') + logger.debug(f'anchor_size = {self.anchor_size}') + +class BaseSceneHandler: + def __init__(self, attr: device_process_attr) -> None: + self.attr = "" + self.__ctx = "" + self.__send_thread = "" + self.__recv_thread = "" + self.__ipe_thread = "" + self.__demux_hdl = "" + self.codec_para = "" + self.__vdec_hdl = "" + self.__vdec_attr = sdk.lyn_vdec_attr_t() + self.__send_queue = block_queue() + self.__ipe_queue = block_queue() + self.__send_num = 0 + self.__recv_num = 0 + # 创建上下文环境 + self.attr = copy.copy(attr) + self.video_frame = attr.video_frame + self.__ctx, ret = sdk.lyn_create_context(self.attr.device_id) + self.enc_head_flag = True + common.error_check(ret, "create context") + ret = sdk.lyn_register_error_handler(common.default_stream_error_handle) + common.error_check(ret, 'lyn_register_error_handler') + + # 打开解封装器 + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + common.error_check(ret, "lyn_demux_open ") + self.codec_para, ret = sdk.lyn_demux_get_codec_para(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_codec_para ") + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_demux_get_framerate") + logger.debug(f'fps = {self.fps}') + + # 打开解码器 + self.__vdec_attr.codec_id = self.codec_para.codec_id + self.__vdec_attr.output_fmt = self.attr.output_fmt + self.__vdec_attr.scale = self.attr.scale + self.__vdec_hdl, ret = sdk.lyn_vdec_open(self.__vdec_attr) + common.error_check(ret, "lyn_vdec_open ") + self.vdec_out_info, ret = sdk.lyn_vdec_get_out_info( + self.codec_para, self.__vdec_attr + ) + logger.debug(f'self.vdec_out_info.width = {self.vdec_out_info.width}') + logger.debug(f'self.vdec_out_info.height = {self.vdec_out_info.height}') + common.error_check(ret, "lyn_vdec_get_out_info ") + self.attr.width = self.vdec_out_info.width + self.attr.height = self.vdec_out_info.height + + # 创建stream、event和ipe desc + self.send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.ipe_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_recv_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + self.venc_send_stream, ret = sdk.lyn_create_stream() + common.error_check(ret, "lyn_create_stream") + + self.ipe_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + self.plugin_event, ret = sdk.lyn_create_event() + common.error_check(ret, "lyn_create_event") + + self.ipe_input_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_input_desc") + self.ipe_output_desc, ret = sdk.lyn_ipe_create_pic_desc() + common.error_check(ret, "lyn_ipe_create_pic_desc ipe_output_desc") + self.ipe_config, ret = sdk.lyn_ipe_create_config_desc() + common.error_check(ret, "lyn_ipe_create_config_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_reset_pic_desc") + ret = sdk.lyn_ipe_reset_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_reset_config_desc") + + # 获取模型信息 + scene_model_configs = [] + self.model_infos = [] + # self.model_infos = [ + # SceneModelHandler( + # model_plugin= '', + # plugin_path='', + # model_code='', + # model_name='', + # objects=[ + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code=''), + # ModelObject(object_code='',object_name='',alarm_threshold='',conf_threshold=0.5,model_code='') + # ], + # image_width=self.attr.width, + # image_height=self.attr.height, + # ) + # ] + self.model_nums = len(self.model_infos) + + + + self.batch_size = self.model_infos[0].batch_size if self.model_nums > 0 else 1 + self.model_width = self.model_infos[0].model_width if self.model_nums > 0 else 640 + self.model_height = self.model_infos[0].model_height if self.model_nums > 0 else 640 + # self.apu_output_size = self.model_infos[0].apu_output_size if self.model_nums > 0 else 1 + + logger.debug(f'self.batch_size = {self.batch_size}') + logger.debug(f'self.model_width = {self.model_width}') + logger.debug(f'self.model_height = {self.model_height}') + # print(f'self.apu_output_size = {self.apu_output_size}') + + self.ipe_output_size = ( + self.model_width + * self.model_height + * (self.model_infos[0].model_desc.inputTensorAttrArray[0].dims[3] if self.model_nums > 0 else 3) + ) + + ( + self.resize_width, + self.resize_height, + self.pad_x, + self.pad_y, + ) = set_padding_data( + self.vdec_out_info.width, + self.vdec_out_info.height, + self.model_width, + self.model_height, + ) + + + # 创建对象池 + self.ipe_output_mem_pool = bufferpool.buffer_pool( + self.ipe_output_size * self.batch_size, 5 + ) + + self.venc_recv_pool = bufferpool.buffer_pool( + self.vdec_out_info.predict_buf_size, 5 + ) + logger.debug(f'self.ipe_output_size = {self.ipe_output_size}') + logger.debug(f'self.vdec_out_info.predict_buf_size = {self.vdec_out_info.predict_buf_size}') + + # 设置编码参数 + self.venc_attr = sdk.lyn_venc_attr_t() + ret = sdk.lyn_venc_set_default_params(self.venc_attr) + common.error_check(ret, "lyn_venc_set_default_params") + self.venc_attr.codec_type = sdk.lyn_codec_id_t.LYN_CODEC_ID_H264 + self.venc_attr.width = self.vdec_out_info.width + self.venc_attr.height = self.vdec_out_info.height + self.venc_attr.bit_depth = 8 + self.venc_attr.bframes_num = 0 + self.venc_attr.pframes_num = 5 + self.venc_attr.input_format = sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12 + self.venc_attr.target_bitrate = 6000000 + self.venc_attr.level = -1 + self.venc_handle, ret = sdk.lyn_venc_open(self.venc_attr) + common.error_check(ret, "lyn_vdec_open") + + self.total_boxes_info, ret = sdk.lyn_malloc(ctypes.sizeof(Box)) + self.start_process_time = None + self.last_push_time = None + self.stream_out_process = None + self.init_output_process() + + + self.frame_step = 5 + self.frame_idx = self.frame_step - 1 # 为了处理第0帧,这里不从0开始计数,而是从step-1开始 + + self.last_alarm_time = {} #key: model_code-objcet_code, value: time + + + def init_output_process(self): + command = ['ffmpeg', + '-i', '-', + "-c:v", "copy", + '-f', 'rtsp', + '-rtsp_transport', 'tcp', + self.attr.output_path] + + # 启动FFmpeg子进程 + try: + self.stream_out_process = subprocess.Popen(command, stdin=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError as e: + logger.exception(f"Failed to start process: {e}") + except subprocess.SubprocessError as e: + logger.exception(f"Subprocess error: {e}") + except Exception as e: + logger.exception(f"An unexpected error occurred: {e}") + + # stream_out_process = ( + # ffmpeg + # .input('pipe:', format='rawvideo', pix_fmt='bgr24', s='640x360', r=25) + # .output(output_path, vcodec='libx264', + # preset='ultrafast', tune='zerolatency', + # bitrate='1000k',maxrate='1000k', bufsize='2000k', f='rtsp',g=10) + # .run_async(pipe_stdin=True, pipe_stderr=True,overwrite_output=True, cmd=['ffmpeg', '-report']) + # ) + + def read_stderr(process): + for line in iter(process.stderr.readline, b''): + logger.info(f"stderr: {line.decode('utf-8').strip()}") + + if self.stream_out_process: + stderr_thread = threading.Thread(target=read_stderr, args=(self.stream_out_process,)) + stderr_thread.daemon = True + stderr_thread.start() + + + def push_video(self): + # show window + global cancel_flag + + frame_rate = self.fps # 目标帧率 + frame_interval = 1 / frame_rate # 每帧间隔时间 + + if not self.last_push_time: + self.last_push_time = time.time() + + while True: + try: + frame = self.video_frame.get(False) + except: + time.sleep(0.01) + continue + if frame[1]: + # cv2.destroyAllWindows() + sys.exit() + + if frame and self.stream_out_process: + image = frame[0] + # resized_image = cv2.resize(image, (640, 360)) + + self.stream_out_process.stdin.write(image.tobytes()) + # elapsed_time = time.time() - self.start_process_time + elapsed_time = time.time() - self.last_push_time + sleep_time = frame_interval - elapsed_time - 0.01 + # print(f'elapsed_time = {elapsed_time*1000:.3f}ms, sleep_time = {sleep_time*1000:.3f}ms') + time.sleep(max(0, sleep_time)) + self.last_push_time = time.time() + + def run(self): + # 开启发送线程 + self.__send_thread = threading.Thread( + target=self.send_thread_func, args=() + ) + self.__send_thread.start() + + # 开启接收线程 + self.__recv_thread = threading.Thread( + target=self.recv_thread_func, args=() + ) + self.__recv_thread.start() + + # 开启图像处理和推理线程 + self.__ipe_thread = threading.Thread( + target=self.ipe_thread_func, args=() + ) + self.__ipe_thread.start() + + def close(self): + if self.__send_thread.is_alive(): + self.__send_thread.join() + if self.__recv_thread.is_alive(): + self.__recv_thread.join() + if self.__ipe_thread.is_alive(): + self.__ipe_thread.join() + + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.ipe_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.apu_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.plugin_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_synchronize_stream") + ret = sdk.lyn_synchronize_stream(self.venc_send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + ret = sdk.lyn_destroy_stream(self.send_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.ipe_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.apu_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.plugin_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_recv_stream) + common.error_check(ret, "lyn_destroy_stream") + ret = sdk.lyn_destroy_stream(self.venc_send_stream) + common.error_check(ret, "lyn_destroy_stream") + + ret = sdk.lyn_destroy_event(self.ipe_event) + common.error_check(ret, "lyn_destroy_event") + ret = sdk.lyn_destroy_event(self.apu_event) + common.error_check(ret, "lyn_destroy_event") + + # destory ipe desc and config + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_input_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_pic_desc(self.ipe_output_desc) + common.error_check(ret, "lyn_ipe_destroy_pic_desc") + ret = sdk.lyn_ipe_destroy_config_desc(self.ipe_config) + common.error_check(ret, "lyn_ipe_destroy_config_desc") + ret = sdk.lyn_plugin_unregister(self.plugin) + common.error_check(ret, "lyn_plugin_unregister") + + # 卸载模型 + + for model_info in self.model_infos: + ret = sdk.lyn_unload_model(model_info.model) + common.error_check(ret, "lyn_unload_model") + + if self.__vdec_hdl != "": + ret = sdk.lyn_vdec_close(self.__vdec_hdl) + common.error_check(ret, "lyn_vdec_close") + + if self.__ctx != "": + ret = sdk.lyn_destroy_context(self.__ctx) + common.error_check(ret, "lyn_destroy_context") + + + def send_thread_func(self): + # 设置上下文环境 创建发送stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + # 从解封装器读取一个包 + pkt, ret = sdk.lyn_demux_read_packet(self.__demux_hdl) + eos = pkt.eos + if ret != 0 and not eos: + sdk.lyn_demux_close(self.__demux_hdl) + time.sleep(500.0 / 1000) + logger.warning("demux failed, reconnecting...") + self.__demux_hdl, ret = sdk.lyn_demux_open(self.attr.url) + + if not ret: + self.fps, ret = sdk.lyn_demux_get_framerate(self.__demux_hdl) + common.error_check(ret, "lyn_synchronize_stream") + logger.debug(f'fps = {self.fps}') + continue + + # 发送给解码器解码 + ret = sdk.lyn_vdec_send_packet_async(self.send_stream, self.__vdec_hdl, pkt) + common.error_check(ret, "lyn_vdec_send_packet_async") + ret = sdk.lyn_synchronize_stream(self.send_stream) + common.error_check(ret, "lyn_synchronize_stream") + + # 释放packet内存并通知接收结果 + if not eos: + sdk.lyn_demux_free_packet(pkt) + self.__send_num += 1 + self.__send_queue.put(self.__send_num) + else: + self.__send_queue.put(-1) + + + def recv_thread_func(self): + # 设置上下文环境 创建接收stream + sdk.lyn_set_current_context(self.__ctx) + frame_pool = bufferpool.buffer_pool(self.vdec_out_info.predict_buf_size, 5) + while self.__recv_num >= 0: + self.__recv_num = self.__send_queue.take() + + cb_data = recv_cb_data() + cb_data.frame.eos = self.__recv_num < 0 + cb_data.frame.data = frame_pool.pop() + cb_data.frame.size = self.vdec_out_info.predict_buf_size + cb_data.frame_pool = frame_pool + cb_data.send_num = self.__send_num + cb_data.recv_num = self.__recv_num + cb_data.attr = self.attr + cb_data.block_queue = self.__ipe_queue + cb_data.video_frame = self.video_frame + # 插入接收指令,并添加接收完成回调函数 + ret = sdk.lyn_vdec_recv_frame_async( + self.recv_stream, self.__vdec_hdl, cb_data.frame + ) + common.error_check(ret, "lyn_vdec_recv_frame_async") + ret = sdk.lyn_stream_add_async_callback( + self.recv_stream, recv_frame_cb, cb_data + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_thread_func(self): + # 设置上下文环境 创建ipe stream + sdk.lyn_set_current_context(self.__ctx) + eos = False + while not eos: + cb_data = self.__ipe_queue.take() + eos = cb_data.frame.eos + self.start_process_time = time.time() + self.model_process(cb_data) # 这里的cb_data是从视频流解析出来的视频帧 + + def model_process(self, cb_data): + self.frame_idx = (self.frame_idx + 1) % self.frame_step + if self.frame_idx == 0: + # print(f'{self.frame_idx} process') + # 正常处理 + if self.model_nums > 0: + ipe_out_data = self.ipe_process(cb_data) + for model_info in self.model_infos: + apu_output_data = self.apu_process(model_info, ipe_out_data) + self.plugin_process(model_info, apu_output_data, cb_data) + + # ret = sdk.lyn_stream_add_async_callback( + # self.model_infos[-1].plugin_stream, + # show_video_cb, + # [cb_data, self.model_infos,self.last_alarm_time], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + + # self.merge_plugin_process(cb_data) + self.encode_process(cb_data) + + common.print_frame_rate(self.get_channel_info()) + + if self.model_nums > 0: + ret = sdk.lyn_stream_add_async_callback( + self.model_infos[-1].apu_stream, + free_to_pool_callback, + [self.ipe_output_mem_pool, ipe_out_data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + else: + # print(f'{self.frame_idx} draw') + # 跳过ipe apu处理,直接用上次的box渲染 + for model_info in self.model_infos: + self.plugin_draw_process(model_info, cb_data) + self.encode_process(cb_data) + common.print_frame_rate(self.get_channel_info()) + + def plugin_process(self,model_info, apu_output_data, cb_data): + pass + + def plugin_draw_process(self,model_info, cb_data): + format = int(sdk.lyn_pixel_format_t.LYN_PIX_FMT_NV12) + pythonapi.PyCapsule_GetPointer.restype = c_void_p + pythonapi.PyCapsule_GetPointer.argtypes = [py_object, c_char_p] + + frame_data_ptr = pythonapi.PyCapsule_GetPointer(cb_data.frame.data, None) + boxes_info_ptr = pythonapi.PyCapsule_GetPointer(model_info.boxes_info, None) + + draw_para = struct.pack( + 'P2IiP', + boxes_info_ptr, + self.codec_para.width, + self.codec_para.height, + format, + frame_data_ptr, + ) + ret = sdk.lyn_plugin_run_async( + model_info.plugin_stream, + model_info.plugin, + "lynDrawBoxAndText", + draw_para, + len(draw_para), + ) + common.error_check(ret, "lyn_plugin_run_async") + + def encode_process(self, cb_data: cb_data): + if self.model_nums > 0: + # 在 plugin 处理完之后,进行编码操作 + ret = sdk.lyn_record_event(self.model_infos[-1].plugin_stream, self.plugin_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(self.venc_send_stream, self.plugin_event) + common.error_check(ret, "lyn_stream_wait_event") + + if self.enc_head_flag: # 第一帧编码 + self.enc_head_flag = False + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.output_path = self.attr.output_path + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + + ret = sdk.lyn_venc_get_paramsset_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_get_paramsset_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_venc_sendframe_async( + self.venc_send_stream, self.venc_handle, cb_data.frame + ) + common.error_check(ret, "lyn_venc_sendframe_async") + + enc_packet = sdk.lyn_packet_t() + enc_packet.size = self.vdec_out_info.predict_buf_size + enc_packet.eos = cb_data.frame.eos + enc_packet.data = self.venc_recv_pool.pop() + encode_data = save_file_cb_data() + encode_data.packet = enc_packet + encode_data.recv_pool = self.venc_recv_pool + encode_data.file_path = self.attr.output_path + encode_data.output_path = self.attr.output_path + encode_data.video_frame = self.attr.video_frame + # ret = sdk.lyn_stream_add_async_callback( + # self.venc_send_stream, + # free_to_pool_callback, + # [cb_data.frame_pool, cb_data.frame.data], + # ) + # common.error_check(ret, "lyn_stream_add_async_callback") + ret = sdk.lyn_venc_recvpacket_async( + self.venc_recv_stream, self.venc_handle, enc_packet + ) + common.error_check(ret, "lyn_venc_recvpacket_async") + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, save_file_cb, [encode_data, cb_data] + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + ret = sdk.lyn_stream_add_async_callback( + self.venc_recv_stream, + free_to_pool_callback, + [cb_data.frame_pool, cb_data.frame.data], + ) + common.error_check(ret, "lyn_stream_add_async_callback") + + + def ipe_process(self, cb_data: framepool_cb_data): + sdk.lyn_set_current_context(self.__ctx) + ipe_out_data = self.ipe_output_mem_pool.pop() + # 设置ipe输入 + ret = sdk.lyn_ipe_set_input_pic_desc( + self.ipe_input_desc, + cb_data.frame.data, + self.vdec_out_info.width, + self.vdec_out_info.height, + self.__vdec_attr.output_fmt, + ) + common.error_check(ret, "lyn_ipe_set_input_pic_desc") + ret = sdk.lyn_ipe_set_output_pic_data(self.ipe_output_desc, ipe_out_data) + common.error_check(ret, "lyn_ipe_set_output_pic_data") + ret = sdk.lyn_ipe_set_resize_config( + self.ipe_config, self.resize_width, self.resize_height + ) + common.error_check(ret, "lyn_ipe_set_resize_config") + ret = sdk.lyn_ipe_set_pad_config( + self.ipe_config, + self.pad_y, + self.pad_x, + self.pad_y, + self.pad_x, + 114, + 114, + 114, + ) + common.error_check(ret, "lyn_ipe_set_pad_config") + ret = sdk.lyn_ipe_set_c2c_config( + self.ipe_config, sdk.lyn_pixel_format_t.LYN_PIX_FMT_RGB24, 0 + ) + common.error_check(ret, "lyn_ipe_set_c2c_config") + ret = sdk.lyn_ipe_cal_output_pic_desc( + self.ipe_output_desc, self.ipe_input_desc, self.ipe_config, 0 + ) + common.error_check(ret, "lyn_ipe_cal_output_pic_desc") + ret = sdk.lyn_ipe_process_async( + self.ipe_stream, self.ipe_input_desc, self.ipe_output_desc, self.ipe_config + ) + common.error_check(ret, "lyn_ipe_process_async") + return ipe_out_data + + + def apu_process(self, model_info, ipe_out_data): + # 等待IPE处理完成 + ret = sdk.lyn_record_event(self.ipe_stream, self.ipe_event) + common.error_check(ret, "lyn_record_event") + ret = sdk.lyn_stream_wait_event(model_info.apu_stream, self.ipe_event) + common.error_check(ret, "lyn_stream_wait_event") + apu_output_data = model_info.apu_output_mem_pool.pop() + ret = sdk.lyn_execute_model_async( + model_info.apu_stream, + model_info.model, + ipe_out_data, + apu_output_data, + self.batch_size, + ) + common.error_check(ret, "lyn_execute_model_async") + return apu_output_data + + def get_channel_info(self) -> str: + return f'{self.attr.device_id}_{self.attr.output_path}' \ No newline at end of file diff --git a/scene_runner.py b/scene_runner.py new file mode 100644 index 0000000..76241cd --- /dev/null +++ b/scene_runner.py @@ -0,0 +1,45 @@ +import threading +import queue +import sys, os + +import common +import bufferpool +from blockqueue import block_queue +from callback_data_struct import * +from info_query import SceneInfo +from string_utils import camel_to_snake, get_class, snake_to_camel +from global_logger import logger + +# 需要全局的http tool? +def run_scene_device(device_no:str, input_url: str, output_url:str, device_id: int, scene_info:SceneInfo, alarm_interval, threads) -> None: + attr = device_process_attr() + attr.device_no = device_no + attr.url = input_url + attr.output_path = output_url + attr.device_id = device_id + attr.alarm_interval = alarm_interval + attr.video_frame = queue.Queue(10) + attr.scene_info = scene_info + if not attr.url: + raise ValueError('input file miss!!!') + if not attr.output_path: + raise ValueError('unspecified output path!!!') + + handle_task_name = scene_info.handle_task + if handle_task_name: + try: + handler_cls = get_class(f'scene_handler.{handle_task_name}', snake_to_camel(handle_task_name)) + scene_instance = handler_cls(attr) + + infer_thread = threading.Thread(target=scene_instance.run, args=()) + threads.append(infer_thread) + infer_thread.start() + + out_thread = threading.Thread(target=scene_instance.push_video, args=()) + threads.append(out_thread) + out_thread.start() + except Exception as e: + # logger.exception(e) + logger.exception(f'scene start failed: {e}') + else: + logger.warning(f'scene {scene_info.remark} for device {device_no} start failed beause no handle task') \ No newline at end of file diff --git a/server.py b/server.py index cc4cd49..529dca9 100644 --- a/server.py +++ b/server.py @@ -1,26 +1,98 @@ from http.server import BaseHTTPRequestHandler, HTTPServer +import json import os import subprocess +import time + +from constants import BOX_PRIVATE_KEY +from rsa_utils import decrypt_message_with_private_key +from urllib.parse import urlparse, parse_qs + +from global_logger import logger + +def check_secret(encrypt_message): + try: + message = decrypt_message_with_private_key(BOX_PRIVATE_KEY, encrypt_message) + command, timestamp = message.rsplit('_', 1) + current_time_ms = int(time.time() * 1000) + time_difference = current_time_ms - int(timestamp) + if time_difference <= 60 * 1000 * 5: + return command + else: + return 'timeout' + except Exception as e: + logger.exception(f"Failed to check secret: {e}") + return None + +def reboot(): + # 执行重启命令 + logger.info('trying to reboot') + try: + # 判断操作系统类型并执行相应的重启命令 + if os.name == "nt": # Windows 系统 + subprocess.run(["shutdown", "/r", "/t", "0"]) + else: # Linux / macOS + subprocess.run(["sudo", "reboot"]) + except Exception as e: + logger.exception(f"Failed to reboot: {e}") + +def sync_data(): + # todo + pass + # 定义HTTP请求处理器 class RequestHandler(BaseHTTPRequestHandler): - def do_GET(self): - # 检查路径是否为 /reboot - if self.path == "/reboot": - self.send_response(200) + def do_POST(self): + # 解析 URL 查询参数 + parsed_url = urlparse(self.path) + query_params = parse_qs(parsed_url.query) + + # 读取并解析请求体 JSON 数据 + content_length = int(self.headers.get('Content-Length', 0)) + post_data = self.rfile.read(content_length) + try: + if self.headers.get("Content-Type") == "application/json": + json_data = json.loads(post_data.decode('utf-8')) if post_data else {} + else: + json_data = {} + except json.JSONDecodeError: + self.send_response(400) self.send_header("Content-type", "application/json") self.end_headers() - self.wfile.write(b'{"message": "Rebooting system..."}') + self.wfile.write(b'{"message": "Invalid JSON format"}') + return + + # 检查路径是否为 /command + if parsed_url.path == "/command": + self.send_response(200) + self.send_header("Content-type", "application/json") - # 执行重启命令 - try: - # 判断操作系统类型并执行相应的重启命令 - if os.name == "nt": # Windows 系统 - subprocess.run(["shutdown", "/r", "/t", "0"]) - else: # Linux / macOS - subprocess.run(["sudo", "reboot"]) - except Exception as e: - print(f"Failed to reboot: {e}") + res_data = {} + secret = query_params.get("secret", [None])[0] + secret = json_data['secret'] + command = check_secret(secret) + logger.info(f'command={command}') + if not command: + res_data['code'] = 500 + res_data['message'] = 'Failed to decrypt' + elif command == 'timeout': + res_data['code'] = 500 + res_data['message'] = 'invalid timestamp' + else: + res_data['code'] = 200 + res_data['message'] = 'success' + + response_body = json.dumps(res_data).encode('utf-8') + self.send_header("Content-Length", str(len(response_body))) + self.end_headers() + self.wfile.write(response_body) + + if command == 'restart': + reboot() + if command == 'sync': + sync_data() + else: # 非 /reboot 请求返回 404 self.send_response(404) @@ -31,11 +103,11 @@ def run_server(host="0.0.0.0", port=5000): server_address = (host, port) httpd = HTTPServer(server_address, RequestHandler) - print(f"HTTP server running on {host}:{port}") + logger.info(f"HTTP server running on {host}:{port}") try: httpd.serve_forever() except KeyboardInterrupt: - print("\nServer is stopping...") + logger.error("\nServer is stopping...") httpd.server_close() if __name__ == "__main__": diff --git a/string_utils.py b/string_utils.py new file mode 100644 index 0000000..058d28e --- /dev/null +++ b/string_utils.py @@ -0,0 +1,29 @@ +import importlib +import re +from datetime import datetime + + +def get_class(module_name, class_name): + # 动态导入模块 + module = importlib.import_module(module_name) + # 使用 getattr 从模块中获取类 + return getattr(module, class_name) + +def get_fun(module_name, function_name): + module = importlib.import_module(module_name) + return getattr(module, function_name) + +def camel_to_snake(name): + # 将大写字母前加上下划线,并将整个字符串转换为小写(驼峰转下划线) + return re.sub(r'(?