From 96ba7c098a107094e6f2c9f8411746aadd82f40f Mon Sep 17 00:00:00 2001 From: root <13910913995@163.com> Date: Sun, 16 Nov 2025 11:43:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=E4=B8=A4=E4=B8=AA?= =?UTF-8?q?=E7=9B=B8=E6=9C=BA=E7=9A=84=E6=94=AF=E6=8C=81=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...tivation_request_W10-D13710C7BD317C29.json | 15 - backend/config.ini | 12 +- backend/database.py | 867 ++++++++---------- backend/devices/camera_manager.py | 60 +- backend/devices/device_coordinator.py | 181 +++- backend/devices/screen_recorder.py | 137 ++- backend/devices/test/devicetest.py | 6 +- backend/devices/test_config_api.py | 227 ----- backend/devices/utils/config_manager.py | 176 +++- backend/main.py | 359 ++++---- backend/utils.py | 9 +- document/Web接口调用说明.md | 259 ++++++ frontend/src/renderer/src/views/Detection.vue | 65 +- .../src/renderer/src/views/PatientCreate.vue | 18 +- 14 files changed, 1323 insertions(+), 1068 deletions(-) delete mode 100644 backend/activation_request_W10-D13710C7BD317C29.json delete mode 100644 backend/devices/test_config_api.py create mode 100644 document/Web接口调用说明.md diff --git a/backend/activation_request_W10-D13710C7BD317C29.json b/backend/activation_request_W10-D13710C7BD317C29.json deleted file mode 100644 index 99cf1497..00000000 --- a/backend/activation_request_W10-D13710C7BD317C29.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "product": "BodyBalanceEvaluation", - "version": "1.0.0", - "machine_id": "W10-D13710C7BD317C29", - "platform": "Windows", - "request_time": "2025-11-04T05:35:19.472181+00:00", - "hardware_info": { - "system": "Windows", - "machine": "AMD64", - "processor": "Intel64 Family 6 Model 165 Stepping 2, GenuineIntel", - "node": "MSI" - }, - "company_name": "北京天宏博科技有限公司", - "contact_info": "thb@163.com" -} \ No newline at end of file diff --git a/backend/config.ini b/backend/config.ini index d25cb646..e0b3b986 100644 --- a/backend/config.ini +++ b/backend/config.ini @@ -17,7 +17,7 @@ max_backups = 7 [FILEPATH] path = D:/BodyCheck/file/ -[CAMERA] +[CAMERA1] enabled = True device_index = 0 width = 1280 @@ -27,6 +27,16 @@ buffer_size = 1 fourcc = MJPG backend = directshow +[CAMERA2] +enabled = True +device_index = 1 +width = 1280 +height = 720 +fps = 30 +buffer_size = 1 +fourcc = MJPG +backend = directshow + [FEMTOBOLT] enabled = True algorithm_type = plt diff --git a/backend/database.py b/backend/database.py index abb09be5..a412cdbe 100644 --- a/backend/database.py +++ b/backend/database.py @@ -9,7 +9,7 @@ import sqlite3 import json import uuid from datetime import datetime, timezone, timedelta -from typing import List, Dict, Optional, Any +from typing import List, Dict, Optional, Any, Union import logging logger = logging.getLogger(__name__) @@ -160,49 +160,7 @@ class DatabaseManager: # 如果生成失败,使用UUID作为备用方案 return str(uuid.uuid4()) - def generate_detection_data_id(self) -> str: - """生成检测数据记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - # 获取当前时间 - china_tz = timezone(timedelta(hours=8)) - - # 使用循环确保生成唯一ID,防止并发冲突 - for attempt in range(10): # 最多尝试10次 - current_time = datetime.now(china_tz) - data_id = current_time.strftime('%Y%m%d%H%M%S') - - # 检查ID是否已存在 - cursor.execute('SELECT COUNT(*) FROM detection_data WHERE id = ?', (data_id,)) - if cursor.fetchone()[0] == 0: - return data_id - - # 如果存在冲突,等待1毫秒后重试 - import time - time.sleep(0.001) - - # 如果10次尝试都失败,在时间后添加随机后缀 - import random - current_time = datetime.now(china_tz) - base_id = current_time.strftime('%Y%m%d%H%M%S') - suffix = str(random.randint(100, 999)) - data_id = f'{base_id}{suffix}' - - # 最后检查一次 - cursor.execute('SELECT COUNT(*) FROM detection_data WHERE id = ?', (data_id,)) - if cursor.fetchone()[0] == 0: - return data_id - - # 如果还是冲突,使用UUID作为备用方案 - logger.warning('检测数据ID生成重试次数过多,使用UUID备用方案') - return str(uuid.uuid4()) - - except Exception as e: - logger.error(f'生成检测数据ID失败: {e}') - # 如果生成失败,使用UUID作为备用方案 - return str(uuid.uuid4()) + def get_connection(self) -> sqlite3.Connection: """获取数据库连接""" @@ -253,6 +211,7 @@ class DatabaseManager: email TEXT, -- 电子邮箱 occupation TEXT, -- 职业 workplace TEXT, -- 工作单位 + idcode TEXT, -- 身份证号 medical_history TEXT, -- 病史 notes TEXT, -- 备注信息 created_at TIMESTAMP, -- 记录创建时间 @@ -269,37 +228,47 @@ class DatabaseManager: start_time TIMESTAMP, -- 检测开始时间 end_time TIMESTAMP, -- 检测结束时间 duration INTEGER, -- 检测持续时间(秒) - settings TEXT, -- 检测设置(JSON格式) - normal_video_path TEXT, -- 足部检测视频文件路径 - femtobolt_video_path TEXT, -- 深度相机视频文件路径 - screen_video_path TEXT, -- 屏幕录制视频路径 diagnosis_info TEXT, -- 诊断信息 treatment_info TEXT, -- 处理信息 - suggestion_info TEXT, -- 建议信息 - status TEXT DEFAULT 'created', -- 会话状态(created/running/checked/diagnosed/completed) + remark_info TEXT, -- 备注信息 + detection_report TEXT, -- 生成检测报告的存储路径 + status TEXT DEFAULT 'checking', -- 会话状态(checking/checked/diagnosed/reported) created_at TIMESTAMP, -- 记录创建时间 FOREIGN KEY (patient_id) REFERENCES patients (id), -- 患者表外键约束 FOREIGN KEY (creator_id) REFERENCES users (id) -- 用户表外键约束 ) ''') - # 创建检测数据表 + # 创建检测截图数据表 cursor.execute(''' CREATE TABLE IF NOT EXISTS detection_data ( id TEXT PRIMARY KEY, -- 记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒 session_id TEXT NOT NULL, -- 检测会话ID(外键) head_pose TEXT , -- 头部姿态数据(JSON格式) body_pose TEXT , -- 身体姿态数据(JSON格式) - body_image TEXT, -- 身体视频截图存储路径 - foot_data TEXT , -- 足部姿态数据(JSON格式) - foot_image TEXT, -- 足部监测视频截图存储路径 - foot_data_image TEXT, -- 足底压力数据图存储路径 - screen_image TEXT, -- 屏幕录制视频截图存储路径 + foot_data TEXT , -- 足部姿态数据(JSON格式) + body_image TEXT, -- 身体视频截图存储路径 + screen_image TEXT, -- 屏幕录制视频截图存储路径 + foot_data_image TEXT, -- 足底压力数据图存储路径 + foot1_image TEXT, -- 足部监测视频1截图存储路径 + foot2_image TEXT, -- 足部监测视频2截图存储路径 timestamp TIMESTAMP, -- 数据记录时间戳 FOREIGN KEY (session_id) REFERENCES detection_sessions (id) -- 检测会话表外键约束 ) ''') - + # 创建检测视频录制表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS detection_video ( + id TEXT PRIMARY KEY, -- 记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒 + session_id TEXT NOT NULL, -- 检测会话ID(外键) + screen_video TEXT, -- 屏幕录制视频存储路径 + body_video TEXT, -- 身体监测视频存储路径 + foot_video1 TEXT, -- 足部监测视频1存储路径 + foot_video2 TEXT, -- 足部监测视频2存储路径 + timestamp TIMESTAMP, -- 数据记录时间戳 + FOREIGN KEY (session_id) REFERENCES detection_sessions (id) -- 检测会话表外键约束 + ) + ''') # 创建系统设置表 cursor.execute(''' @@ -326,10 +295,13 @@ class DatabaseManager: cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_time ON detection_sessions (start_time)') # 开始时间索引 cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_status ON detection_sessions (status)') # 会话状态索引 - # 检测数据表索引 + # 检测截图数据表索引 cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_session ON detection_data (session_id)') # 会话ID索引 cursor.execute('CREATE INDEX IF NOT EXISTS idx_data_timestamp ON detection_data (timestamp)') # 时间戳索引 - + + # 检测视频数据表索引 + cursor.execute('CREATE INDEX IF NOT EXISTS idx_video_session ON detection_video (session_id)') # 会话ID索引 + cursor.execute('CREATE INDEX IF NOT EXISTS idx_video_timestamp ON detection_video (timestamp)') # 时间戳索引 # 用户表索引 cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_username ON users (username)') # 用户名索引 cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_type ON users (user_type)') # 用户类型索引 @@ -352,21 +324,8 @@ class DatabaseManager: VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ''', (admin_id, '系统管理员', 'admin', admin_password, 1, 'admin', china_time, china_time, china_time)) - logger.info('创建默认管理员账户: admin/admin123') - - # 插入默认系统设置 - china_time = self.get_china_time() - - # 检查并插入默认摄像头设备索引配置 - cursor.execute('SELECT COUNT(*) FROM system_settings WHERE key = ?', ('monitor_device_index',)) - monitor_config_exists = cursor.fetchone()[0] - - if monitor_config_exists == 0: - cursor.execute(''' - INSERT INTO system_settings (key, value, description, updated_at) - VALUES (?, ?, ?, ?) - ''', ('monitor_device_index', '1', '足部监视摄像头设备索引号', china_time)) - logger.info('创建默认摄像头设备索引配置: 1') + logger.info('创建默认管理员账户: admin/admin123') + conn.commit() logger.info('数据库初始化完成') @@ -376,7 +335,7 @@ class DatabaseManager: logger.error(f'数据库初始化失败: {e}') raise - # ==================== 患者管理 ==================== + # ==================== 患者数据管理 ====================# def create_patient(self, patient_data: Dict[str, Any]) -> str: """创建患者记录""" @@ -396,8 +355,8 @@ class DatabaseManager: INSERT INTO patients ( id, name, gender, birth_date, nationality, residence, height, weight, shoe_size, phone, email, occupation, workplace, - medical_history, notes, created_at, updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + medical_history,idcode, notes, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,?) ''', ( patient_id, patient_data.get('name'), @@ -413,6 +372,7 @@ class DatabaseManager: patient_data.get('occupation'), patient_data.get('workplace'), patient_data.get('medical_history'), + patient_data.get('idcode'), patient_data.get('notes'), china_time, china_time @@ -443,15 +403,27 @@ class DatabaseManager: if keyword: cursor.execute(''' - SELECT * FROM patients - WHERE name LIKE ? OR phone LIKE ? OR email LIKE ? - ORDER BY created_at DESC + SELECT p.*, COALESCE(ds.session_count, 0) AS detection_count + FROM patients p + LEFT JOIN ( + SELECT patient_id, COUNT(*) AS session_count + FROM detection_sessions + GROUP BY patient_id + ) ds ON ds.patient_id = p.id + WHERE p.name LIKE ? OR p.phone LIKE ? OR p.email LIKE ? + ORDER BY p.created_at DESC LIMIT ? OFFSET ? ''', (f'%{keyword}%', f'%{keyword}%', f'%{keyword}%', size, offset)) else: cursor.execute(''' - SELECT * FROM patients - ORDER BY created_at DESC + SELECT p.*, COALESCE(ds.session_count, 0) AS detection_count + FROM patients p + LEFT JOIN ( + SELECT patient_id, COUNT(*) AS session_count + FROM detection_sessions + GROUP BY patient_id + ) ds ON ds.patient_id = p.id + ORDER BY p.created_at DESC LIMIT ? OFFSET ? ''', (size, offset)) @@ -517,7 +489,7 @@ class DatabaseManager: UPDATE patients SET name = ?, gender = ?, birth_date = ?, nationality = ?, residence = ?, height = ?, weight = ?, shoe_size = ?, phone = ?, email = ?, - occupation = ?, workplace = ?, medical_history = ?, notes = ?, updated_at = ? + occupation = ?, workplace = ?, medical_history = ?, idcode = ?, notes = ?, updated_at = ? WHERE id = ? ''', ( patient_data.get('name'), @@ -533,6 +505,7 @@ class DatabaseManager: patient_data.get('occupation'), patient_data.get('workplace'), patient_data.get('medical_history'), + patient_data.get('idcode'), patient_data.get('notes'), china_time, patient_id @@ -556,13 +529,21 @@ class DatabaseManager: cursor = conn.cursor() try: - # 删除相关的检测数据 + # 删除相关的检测截图数据 cursor.execute(''' DELETE FROM detection_data WHERE session_id IN ( SELECT id FROM detection_sessions WHERE patient_id = ? ) ''', (patient_id,)) + + # 删除相关的检测视频数据 + cursor.execute(''' + DELETE FROM detection_video + WHERE session_id IN ( + SELECT id FROM detection_sessions WHERE patient_id = ? + ) + ''', (patient_id,)) # 删除检测会话 cursor.execute('DELETE FROM detection_sessions WHERE patient_id = ?', (patient_id,)) @@ -576,48 +557,8 @@ class DatabaseManager: except Exception as e: conn.rollback() logger.error(f'删除患者记录失败: {e}') - raise - - def batch_delete_patients(self, patient_ids: List[str]): - """批量删除患者记录""" - if not patient_ids: - return - - conn = self.get_connection() - cursor = conn.cursor() - - try: - # 验证所有患者是否存在 - placeholders = ','.join(['?' for _ in patient_ids]) - cursor.execute(f'SELECT COUNT(*) FROM patients WHERE id IN ({placeholders})', patient_ids) - existing_count = cursor.fetchone()[0] - - if existing_count != len(patient_ids): - raise ValueError('部分患者记录不存在') - - # 批量删除相关的检测数据 - cursor.execute(f''' - DELETE FROM detection_data - WHERE session_id IN ( - SELECT id FROM detection_sessions WHERE patient_id IN ({placeholders}) - ) - ''', patient_ids) - - # 批量删除检测会话 - cursor.execute(f'DELETE FROM detection_sessions WHERE patient_id IN ({placeholders})', patient_ids) - - # 批量删除患者记录 - cursor.execute(f'DELETE FROM patients WHERE id IN ({placeholders})', patient_ids) - - conn.commit() - logger.info(f'批量删除患者记录: {len(patient_ids)}条') - - except Exception as e: - conn.rollback() - logger.error(f'批量删除患者记录失败: {e}') - raise - - # ==================== 检测会话管理 ==================== + raise + # ==================== 检测会话数据管理 ==================== # def create_detection_session(self, patient_id: str, settings: Dict[str, Any], creator_id: str = None) -> str: """创建检测会话""" @@ -632,19 +573,14 @@ class DatabaseManager: cursor.execute(''' INSERT INTO detection_sessions ( - id, patient_id, creator_id, duration, settings, status, - diagnosis_info, treatment_info, suggestion_info, start_time, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + id, patient_id, creator_id, status, + start_time, created_at + ) VALUES (?, ?, ?, ?, ?, ?) ''', ( session_id, patient_id, - creator_id, - settings.get('duration', 60), - json.dumps(settings), - 'checking', - '', - '', - '', + creator_id, + 'checking', china_time, china_time )) @@ -659,189 +595,109 @@ class DatabaseManager: raise def update_session_status(self, session_id: str, status: str) -> bool: - """更新会话状态 - + """更新会话状态 Returns: bool: 更新成功返回True,失败返回False """ conn = self.get_connection() cursor = conn.cursor() - try: # 首先获取会话对应的患者ID cursor.execute('SELECT patient_id FROM detection_sessions WHERE id = ?', (session_id,)) result = cursor.fetchone() if not result: logger.error(f'会话不存在: {session_id}') - return False - - patient_id = result[0] - china_time = self.get_china_time() - - if status in ['checked', 'stopped','complated', 'error']: - # 使用中国时区时间 - cursor.execute(''' - UPDATE detection_sessions SET - status = ?, end_time = ? - WHERE id = ? - ''', (status, china_time, session_id)) - - # 同步更新患者表的updated_at时间 - cursor.execute(''' - UPDATE patients SET updated_at = ? - WHERE id = ? - ''', (china_time, patient_id)) - else: - cursor.execute(''' + return False + cursor.execute(''' UPDATE detection_sessions SET status = ? WHERE id = ? - ''', (status, session_id)) - - - - conn.commit() - logger.info(f'更新会话状态: {session_id} -> {status},同时更新患者 {patient_id} 的updated_at时间') - return True - + ''', (status, session_id)) + conn.commit() + return True except Exception as e: conn.rollback() logger.error(f'更新会话状态失败: {e}') return False - def update_session_duration(self, session_id: str, duration: int): - """更新会话持续时间""" + + def update_session_endcheck(self, session_id: str, diagnosis_info: Optional[str] = None, + treatment_info: Optional[str] = None, + suggestion_info: Optional[str] = None) -> bool: + """结束检测:根据 start_time 与当前时间计算持续时长并写入结束信息 + + 行为: + - 计算并保存 `duration` + - 保存 `end_time` 为当前中国时区时间 + - 可选更新 `diagnosis_info`、`treatment_info`、`suggestion_info` + - 设置 `status = 'checked'` + - 同步更新患者 `updated_at` + """ conn = self.get_connection() cursor = conn.cursor() - + try: - cursor.execute(''' - UPDATE detection_sessions SET duration = ? - WHERE id = ? - ''', (duration, session_id)) - + # 读取会话的开始时间与患者ID + cursor.execute('SELECT start_time, patient_id FROM detection_sessions WHERE id = ?', (session_id,)) + row = cursor.fetchone() + if not row: + logger.error(f'会话不存在: {session_id}') + return False + + start_time_str, patient_id = row + if not start_time_str: + logger.error(f'会话缺少开始时间: {session_id}') + return False + + # 计算持续时间(秒) + now_str = self.get_china_time() + try: + start_dt = datetime.strptime(start_time_str, '%Y-%m-%d %H:%M:%S') + now_dt = datetime.strptime(now_str, '%Y-%m-%d %H:%M:%S') + duration_seconds = max(0, int((now_dt - start_dt).total_seconds())) + except Exception as e: + logger.error(f'解析时间失败: start_time={start_time_str}, now={now_str}, error={e}') + return False + + # 构造更新语句 + update_fields = [ + 'duration = ?', + 'end_time = ?', + 'status = ?' + ] + update_values = [duration_seconds, now_str, 'checked'] + + if diagnosis_info is not None: + update_fields.append('diagnosis_info = ?') + update_values.append(diagnosis_info) + + if treatment_info is not None: + update_fields.append('treatment_info = ?') + update_values.append(treatment_info) + + if suggestion_info is not None: + update_fields.append('suggestion_info = ?') + update_values.append(suggestion_info) + + # 添加会话ID到参数列表 + update_values.append(session_id) + + sql = f'''UPDATE detection_sessions SET {', '.join(update_fields)} WHERE id = ?''' + cursor.execute(sql, update_values) + + # 同步更新患者表的updated_at时间 + cursor.execute('''UPDATE patients SET updated_at = ? WHERE id = ?''', (now_str, patient_id)) + conn.commit() - logger.info(f'更新会话持续时间: {session_id} -> {duration}秒') - + logger.info(f'结束检测并更新会话: {session_id}, duration={duration_seconds}s, status=checked') + return True + except Exception as e: conn.rollback() - logger.error(f'更新会话持续时间失败: {e}') - raise + logger.error(f'结束检测更新失败: {e}') + return False - def update_session_normal_video_path(self, session_id: str, video_path: str): - """更新会话足部检测视频路径""" - conn = self.get_connection() - cursor = conn.cursor() - try: - cursor.execute(''' - UPDATE detection_sessions SET normal_video_path = ? - WHERE id = ? - ''', (video_path, session_id)) - - conn.commit() - logger.info(f'更新会话足部检测视频路径: {session_id} -> {video_path}') - - except Exception as e: - conn.rollback() - logger.error(f'更新会话足部检测视频路径失败: {e}') - raise - - def update_session_femtobolt_video_path(self, session_id: str, video_path: str): - """更新会话深度相机视频路径""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - cursor.execute(''' - UPDATE detection_sessions SET femtobolt_video_path = ? - WHERE id = ? - ''', (video_path, session_id)) - - conn.commit() - logger.info(f'更新会话深度相机视频路径: {session_id} -> {video_path}') - - except Exception as e: - conn.rollback() - logger.error(f'更新会话深度相机视频路径失败: {e}') - raise - - def update_session_screen_video_path(self, session_id: str, video_path: str): - """更新会话屏幕录制视频路径""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - cursor.execute(''' - UPDATE detection_sessions SET screen_video_path = ? - WHERE id = ? - ''', (video_path, session_id)) - - conn.commit() - logger.info(f'更新会话屏幕录制视频路径: {session_id} -> {video_path}') - - except Exception as e: - conn.rollback() - logger.error(f'更新会话屏幕录制视频路径失败: {e}') - raise - - def update_session_diagnosis_info(self, session_id: str, diagnosis_info: str): - """更新会话诊断信息""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - cursor.execute(''' - UPDATE detection_sessions SET diagnosis_info = ? - WHERE id = ? - ''', (diagnosis_info, session_id)) - - conn.commit() - logger.info(f'更新会话诊断信息: {session_id}') - - except Exception as e: - conn.rollback() - logger.error(f'更新会话诊断信息失败: {e}') - raise - - def update_session_treatment_info(self, session_id: str, treatment_info: str): - """更新会话处理信息""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - cursor.execute(''' - UPDATE detection_sessions SET treatment_info = ? - WHERE id = ? - ''', (treatment_info, session_id)) - - conn.commit() - logger.info(f'更新会话处理信息: {session_id}') - - except Exception as e: - conn.rollback() - logger.error(f'更新会话处理信息失败: {e}') - raise - - def update_session_suggestion_info(self, session_id: str, suggestion_info: str): - """更新会话建议信息""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - cursor.execute(''' - UPDATE detection_sessions SET suggestion_info = ? - WHERE id = ? - ''', (suggestion_info, session_id)) - - conn.commit() - logger.info(f'更新会话建议信息: {session_id}') - - except Exception as e: - conn.rollback() - logger.error(f'更新会话建议信息失败: {e}') - raise - def update_session_all_info(self, session_id: str, diagnosis_info: str = None, treatment_info: str = None, suggestion_info: str = None, status: str = None): """同时更新会话的诊断信息、处理信息、建议信息和状态""" conn = self.get_connection() @@ -867,11 +723,6 @@ class DatabaseManager: if status is not None: update_fields.append('status = ?') update_values.append(status) - - # 如果状态是完成、停止或错误,同时更新结束时间 - if status in ['completed', 'stopped', 'error']: - update_fields.append('end_time = ?') - update_values.append(self.get_china_time()) if not update_fields: logger.warning(f'没有提供要更新的信息: {session_id}') @@ -903,9 +754,7 @@ class DatabaseManager: except Exception as e: conn.rollback() logger.error(f'批量更新会话信息失败: {e}') - raise - - + raise def get_detection_sessions(self, page: int = 1, size: int = 10, patient_id: str = None) -> List[Dict]: """获取检测会话列表""" @@ -913,41 +762,17 @@ class DatabaseManager: cursor = conn.cursor() try: - offset = (page - 1) * size - - if patient_id: - cursor.execute(''' - SELECT s.*, p.name as patient_name, u.name as creator_name - FROM detection_sessions s - LEFT JOIN patients p ON s.patient_id = p.id - LEFT JOIN users u ON s.creator_id = u.id - WHERE s.patient_id = ? - ORDER BY s.start_time DESC - LIMIT ? OFFSET ? - ''', (patient_id, size, offset)) - else: - cursor.execute(''' - SELECT s.*, p.name as patient_name, u.name as creator_name - FROM detection_sessions s - LEFT JOIN patients p ON s.patient_id = p.id - LEFT JOIN users u ON s.creator_id = u.id - ORDER BY s.start_time DESC - LIMIT ? OFFSET ? - ''', (size, offset)) - + offset = (page - 1) * size + cursor.execute(''' + SELECT s.id, s.status, s.start_time, u.name as creator_name + FROM detection_sessions s + LEFT JOIN users u ON s.creator_id = u.id + WHERE s.patient_id = ? + ORDER BY s.start_time DESC + LIMIT ? OFFSET ? + ''', (patient_id, size, offset)) rows = cursor.fetchall() - sessions = [] - - for row in rows: - session = dict(row) - # 解析设置JSON - if session['settings']: - try: - session['settings'] = json.loads(session['settings']) - except: - session['settings'] = {} - sessions.append(session) - + sessions = [] return sessions except Exception as e: @@ -957,16 +782,10 @@ class DatabaseManager: def get_sessions_count(self, patient_id: str = None) -> int: """获取会话总数""" conn = self.get_connection() - cursor = conn.cursor() - + cursor = conn.cursor() try: - if patient_id: - cursor.execute('SELECT COUNT(*) FROM detection_sessions WHERE patient_id = ?', (patient_id,)) - else: - cursor.execute('SELECT COUNT(*) FROM detection_sessions') - - return cursor.fetchone()[0] - + icursor.execute('SELECT COUNT(*) FROM detection_sessions WHERE patient_id = ?', (patient_id,)) + return cursor.fetchone()[0] except Exception as e: logger.error(f'获取会话总数失败: {e}') return 0 @@ -982,15 +801,16 @@ class DatabaseManager: if cursor.fetchone()[0] == 0: raise ValueError(f'检测会话不存在: {session_id}') - # 先删除相关的检测数据 + # 先删除相关的检测截图数据 cursor.execute('DELETE FROM detection_data WHERE session_id = ?', (session_id,)) deleted_data_count = cursor.rowcount - + # 先删除相关的检测视频数据 + cursor.execute('DELETE FROM detection_video WHERE session_id = ?', (session_id,)) + deleted_video_count = cursor.rowcount # 再删除检测会话 - cursor.execute('DELETE FROM detection_sessions WHERE id = ?', (session_id,)) - + cursor.execute('DELETE FROM detection_sessions WHERE id = ?', (session_id,)) conn.commit() - logger.info(f'删除检测会话: {session_id}, 同时删除了 {deleted_data_count} 条检测数据') + logger.info(f'删除检测会话: {session_id}, 同时删除了 {deleted_data_count} 条检测截图数据, {deleted_video_count} 条检测视频数据') except Exception as e: conn.rollback() @@ -1016,14 +836,7 @@ class DatabaseManager: if not session_row: return None - session = dict(session_row) - - # 解析设置JSON - if session['settings']: - try: - session['settings'] = json.loads(session['settings']) - except: - session['settings'] = {} + session = dict(session_row) # 获取检测数据 cursor.execute(''' @@ -1044,156 +857,270 @@ class DatabaseManager: pass session['data'].append(data_point) + # 获取检测视频数据 + cursor.execute(''' + SELECT * FROM detection_video + WHERE session_id = ? + ORDER BY timestamp + ''', (session_id,)) + + video_rows = cursor.fetchall() + session['videos'] = [] + + for vrow in video_rows: + video_item = dict(vrow) + session['videos'].append(video_item) + return session except Exception as e: logger.error(f'获取会话数据失败: {e}') return None - # ==================== 检测记录数据管理 ==================== - - def save_detection_data(self, session_id: str, data: Dict[str, Any]): - """保存检测数据""" + # ==================== 检测截图数据管理 ==================== # + def generate_detection_data_id(self) -> str: + """生成检测数据记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒""" conn = self.get_connection() cursor = conn.cursor() + try: + # 获取当前时间 + china_tz = timezone(timedelta(hours=8)) + + # 使用循环确保生成唯一ID,防止并发冲突 + for attempt in range(10): # 最多尝试10次 + current_time = datetime.now(china_tz) + data_id = current_time.strftime('%Y%m%d%H%M%S') + + # 检查ID是否已存在 + cursor.execute('SELECT COUNT(*) FROM detection_data WHERE id = ?', (data_id,)) + if cursor.fetchone()[0] == 0: + return data_id + + # 如果存在冲突,等待1毫秒后重试 + import time + time.sleep(0.001) + + # 如果10次尝试都失败,在时间后添加随机后缀 + import random + current_time = datetime.now(china_tz) + base_id = current_time.strftime('%Y%m%d%H%M%S') + suffix = str(random.randint(100, 999)) + data_id = f'{base_id}{suffix}' + + # 最后检查一次 + cursor.execute('SELECT COUNT(*) FROM detection_data WHERE id = ?', (data_id,)) + if cursor.fetchone()[0] == 0: + return data_id + + # 如果还是冲突,使用UUID作为备用方案 + logger.warning('检测数据ID生成重试次数过多,使用UUID备用方案') + return str(uuid.uuid4()) + + except Exception as e: + logger.error(f'生成检测数据ID失败: {e}') + # 如果生成失败,使用UUID作为备用方案 + return str(uuid.uuid4()) + + def save_detection_data(self, session_id: str, data: Dict[str, Any]) -> bool: + """保存检测数据(与 detection_data 表结构保持一致)""" + conn = self.get_connection() + cursor = conn.cursor() + try: # 使用中国时区时间 china_time = self.get_china_time() - + # 生成检测数据记录ID data_id = self.generate_detection_data_id() - - # 根据新的表结构保存数据 + + # 根据表结构保存数据 cursor.execute(''' INSERT INTO detection_data ( - id, session_id, head_pose, body_pose, body_image, - foot_data, foot_image, foot_data_image, screen_image, timestamp - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + id, session_id, head_pose, body_pose, foot_data, + body_image, screen_image, foot_data_image, foot1_image, foot2_image, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ''', ( data_id, session_id, json.dumps(data.get('head_pose')) if data.get('head_pose') else None, json.dumps(data.get('body_pose')) if data.get('body_pose') else None, - data.get('body_image'), json.dumps(data.get('foot_data')) if data.get('foot_data') else None, - data.get('foot_image'), - data.get('foot_data_image'), + data.get('body_image'), data.get('screen_image'), + data.get('foot_data_image'), + data.get('foot1_image'), + data.get('foot2_image'), china_time )) - + conn.commit() logger.info(f'保存检测数据: {data_id}') - + return True + except Exception as e: conn.rollback() logger.error(f'保存检测数据失败: {e}') - raise - - def get_latest_detection_data(self, session_id: str, limit: int = 10) -> List[Dict]: - """获取最新的检测数据""" + return False + + def get_detection_data_by_ids(self, data_ids: Union[str, List[str]]) -> List[Dict[str, Any]]: + """根据1个或2个ID查询检测数据记录,返回列表 + + - 支持传入单个字符串ID或ID列表(建议最多2个) + - 解析 `data_value` 字段为对象(若解析失败则保留原字符串) + """ + conn = self.get_connection() + cursor = conn.cursor() + + # 规范化为列表 + ids: List[str] = [data_ids] if isinstance(data_ids, str) else list(data_ids or []) + if not ids: + return [] + + try: + placeholders = ','.join(['?'] * len(ids)) + sql = f'''SELECT * FROM detection_data WHERE id IN ({placeholders}) ORDER BY timestamp''' + cursor.execute(sql, ids) + rows = cursor.fetchall() + + results: List[Dict[str, Any]] = [] + for row in rows: + item = dict(row) + # 解析 JSON 字段 + try: + item['data_value'] = json.loads(item.get('data_value', '{}')) + except Exception: + pass + results.append(item) + + logger.info(f'查询检测数据 {len(results)} 条,IDs: {ids}') + return results + except Exception as e: + logger.error(f'查询检测数据失败: {e}') + return [] + + def delete_detection_data(self, data_ids: Union[str, List[str]]) -> bool: + """删除检测数据记录(支持单个或多个ID)""" conn = self.get_connection() cursor = conn.cursor() - + try: - cursor.execute(''' - SELECT * FROM detection_data - WHERE session_id = ? - ORDER BY timestamp DESC - LIMIT ? - ''', (session_id, limit)) - - rows = cursor.fetchall() - data_points = [] - - for row in rows: - data_point = dict(row) - # 解析JSON字段 - try: - if data_point.get('head_pose'): - data_point['head_pose'] = json.loads(data_point['head_pose']) - except: - pass - try: - if data_point.get('body_pose'): - data_point['body_pose'] = json.loads(data_point['body_pose']) - except: - pass - try: - if data_point.get('foot_data'): - data_point['foot_data'] = json.loads(data_point['foot_data']) - except: - pass - data_points.append(data_point) - - return data_points - - except Exception as e: - logger.error(f'获取最新检测数据失败: {e}') - return [] - - def get_detection_data_by_id(self, data_id: str) -> Optional[Dict]: - """根据主键ID查询检测数据详情""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - cursor.execute(''' - SELECT * FROM detection_data - WHERE id = ? - ''', (data_id,)) - - row = cursor.fetchone() - if not row: - return None - - data_point = dict(row) - # 解析JSON字段 - try: - if data_point.get('head_pose'): - data_point['head_pose'] = json.loads(data_point['head_pose']) - except: - pass - try: - if data_point.get('body_pose'): - data_point['body_pose'] = json.loads(data_point['body_pose']) - except: - pass - try: - if data_point.get('foot_data'): - data_point['foot_data'] = json.loads(data_point['foot_data']) - except: - pass - - return data_point - - except Exception as e: - logger.error(f'根据ID获取检测数据失败: {e}') - return None - - def delete_detection_data(self, data_id: str): - """按主键删除检测数据记录""" - conn = self.get_connection() - cursor = conn.cursor() - - try: - # 验证记录是否存在 - cursor.execute('SELECT COUNT(*) FROM detection_data WHERE id = ?', (data_id,)) - if cursor.fetchone()[0] == 0: - raise ValueError(f'检测数据记录不存在: {data_id}') - - # 删除检测数据记录 - cursor.execute('DELETE FROM detection_data WHERE id = ?', (data_id,)) - + # 规范化为列表 + ids: List[str] = [data_ids] if isinstance(data_ids, str) else list(data_ids or []) + if not ids: + logger.info('未提供需要删除的检测数据ID,跳过') + return True + + placeholders = ','.join(['?'] * len(ids)) + sql = f'DELETE FROM detection_data WHERE id IN ({placeholders})' + cursor.execute(sql, ids) conn.commit() - logger.info(f'删除检测数据记录: {data_id}') - + logger.info(f'删除检测数据 {len(ids)} 条: {ids}') + return True except Exception as e: conn.rollback() - logger.error(f'删除检测数据记录失败: {e}') - raise - + logger.error(f'删除检测数据失败: {e}') + return False + + def has_session_detection_data(self, session_id: str) -> bool: + """检查指定会话是否存在检测数据,用于判断单次检测是否有效""" + conn = self.get_connection() + cursor = conn.cursor() + try: + cursor.execute('SELECT COUNT(1) FROM detection_data WHERE session_id = ?', (session_id,)) + row = cursor.fetchone() + count = row[0] if row else 0 + exists = count > 0 + logger.info(f'会话 {session_id} 检测数据存在: {exists} (count={count})') + return exists + except Exception as e: + logger.error(f'检查会话检测数据存在失败: {e}') + return False + +# ==================== 检测视频数据管理 ==================== # + def generate_detection_video_id(self) -> str: + """生成检测视频记录唯一标识(YYYYMMDDHHMMSS)年月日时分秒""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + china_tz = timezone(timedelta(hours=8)) + for attempt in range(10): + current_time = datetime.now(china_tz) + video_id = current_time.strftime('%Y%m%d%H%M%S') + cursor.execute('SELECT COUNT(*) FROM detection_video WHERE id = ?', (video_id,)) + if cursor.fetchone()[0] == 0: + return video_id + import time + time.sleep(0.001) + + import random + base_id = datetime.now(china_tz).strftime('%Y%m%d%H%M%S') + suffix = str(random.randint(100, 999)) + video_id = f'{base_id}{suffix}' + cursor.execute('SELECT COUNT(*) FROM detection_video WHERE id = ?', (video_id,)) + if cursor.fetchone()[0] == 0: + return video_id + logger.warning('检测视频ID生成重试次数过多,使用UUID备用方案') + return str(uuid.uuid4()) + except Exception as e: + logger.error(f'生成检测视频ID失败: {e}') + return str(uuid.uuid4()) + + def save_detection_video(self, session_id: str, video: Dict[str, Any]) -> bool: + """保存检测视频记录(与 detection_video 表结构保持一致)""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + video_id = self.generate_detection_video_id() + china_time = self.get_china_time() + + cursor.execute(''' + INSERT INTO detection_video ( + id, session_id, screen_video, body_video, foot_video1, foot_video2, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?) + ''', ( + video_id, + session_id, + video.get('screen_video_path'), + video.get('femtobolt_video_path'), + video.get('camera1_video_path'), + video.get('camera2_video_path'), + china_time + )) + + conn.commit() + logger.info(f'保存检测视频: {video_id}') + return True + except Exception as e: + conn.rollback() + logger.error(f'保存检测视频失败: {e}') + return False + + def delete_detection_video(self, video_ids: Union[str, List[str]]) -> bool: + """删除检测视频记录(支持单个或多个ID)""" + conn = self.get_connection() + cursor = conn.cursor() + + try: + # 规范化为列表 + ids: List[str] = [video_ids] if isinstance(video_ids, str) else list(video_ids or []) + if not ids: + logger.info('未提供需要删除的检测视频ID,跳过') + return True + + placeholders = ','.join(['?'] * len(ids)) + sql = f'DELETE FROM detection_video WHERE id IN ({placeholders})' + cursor.execute(sql, ids) + conn.commit() + logger.info(f'删除检测视频 {len(ids)} 条: {ids}') + return True + except Exception as e: + conn.rollback() + logger.error(f'删除检测视频失败: {e}') + return False + # ==================== 系统设置管理 ==================== diff --git a/backend/devices/camera_manager.py b/backend/devices/camera_manager.py index 43926695..220ba2c7 100644 --- a/backend/devices/camera_manager.py +++ b/backend/devices/camera_manager.py @@ -26,34 +26,49 @@ except ImportError: class CameraManager(BaseDevice): """普通相机管理器""" - def __init__(self, socketio, config_manager: Optional[ConfigManager] = None): + def __init__(self, socketio, config_manager: Optional[ConfigManager] = None, + device_name: str = "camera1", + instance_config: Optional[Dict[str, Any]] = None): """ 初始化相机管理器 Args: socketio: SocketIO实例 config_manager: 配置管理器实例 + device_name: 设备名称(仅支持 'camera1' | 'camera2') + instance_config: 覆盖默认配置的实例级配置(如 device_index、分辨率、fps 等) """ # 配置管理 self.config_manager = config_manager or ConfigManager() - config = self.config_manager.get_device_config('camera') + # 校验设备名,仅允许 camera1/camera2 + if device_name not in ('camera1', 'camera2'): + raise ValueError(f"不支持的设备名: {device_name},仅支持 'camera1'/'camera2'") + # 根据设备名选择配置源:'camera1' 使用 [CAMERA1];'camera2' 使用 [CAMERA2] + base_key = 'camera1' if device_name == 'camera1' else 'camera2' + base_config = self.config_manager.get_device_config(base_key) + # 合并实例覆盖配置 + if instance_config: + try: + base_config = {**base_config, **instance_config} + except Exception: + pass - super().__init__("camera", config) + super().__init__(device_name, base_config) # 保存socketio实例 self._socketio = socketio # 相机相关属性 self.cap = None - self.device_index = config.get('device_index', 0) - self.width = config.get('width', 1280) - self.height = config.get('height', 720) - self.fps = config.get('fps', 30) - self.buffer_size = config.get('buffer_size', 1) - self.fourcc = config.get('fourcc', 'MJPG') + self.device_index = base_config.get('device_index', 0) + self.width = base_config.get('width', 1280) + self.height = base_config.get('height', 720) + self.fps = base_config.get('fps', 30) + self.buffer_size = base_config.get('buffer_size', 1) + self.fourcc = base_config.get('fourcc', 'MJPG') # OpenCV后端配置 (DirectShow性能最佳) - backend_name = config.get('backend', 'directshow').lower() + backend_name = base_config.get('backend', 'directshow').lower() self.backend_map = { 'directshow': cv2.CAP_DSHOW, 'dshow': cv2.CAP_DSHOW, @@ -64,12 +79,12 @@ class CameraManager(BaseDevice): self.backend_name = backend_name # 额外可调的降采样宽度(不改变外部配置语义,仅内部优化传输) - self._tx_max_width = int(config.get('tx_max_width', 1920)) + self._tx_max_width = int(base_config.get('tx_max_width', 1920)) # 流控制 self.streaming_thread = None # 减小缓存长度,保留最近2帧即可,避免累计占用 - self.frame_cache = queue.Queue(maxsize=int(config.get('frame_cache_len', 2))) + self.frame_cache = queue.Queue(maxsize=int(base_config.get('frame_cache_len', 2))) self.last_frame = None self.frame_count = 0 self.dropped_frames = 0 @@ -80,13 +95,14 @@ class CameraManager(BaseDevice): self.actual_fps = 0 # 重连与断连检测机制(-1 表示无限重连) - self.max_reconnect_attempts = int(config.get('max_reconnect_attempts', -1)) - self.reconnect_delay = float(config.get('reconnect_delay', 2.0)) - self.read_fail_threshold = int(config.get('read_fail_threshold', 30)) + self.max_reconnect_attempts = int(base_config.get('max_reconnect_attempts', -1)) + self.reconnect_delay = float(base_config.get('reconnect_delay', 2.0)) + self.read_fail_threshold = int(base_config.get('read_fail_threshold', 30)) self._last_connected_state = None # 设备标识和性能统计 - self.device_id = f"camera_{self.device_index}" + # 使用设备名作为ID,便于前端区分 + self.device_id = device_name self.performance_stats = { 'frames_processed': 0, 'actual_fps': 0, @@ -373,6 +389,9 @@ class CameraManager(BaseDevice): total_config_time = (time.time() - config_start) * 1000 + # 若未进行性能优化,确保变量存在 + optimization_time = locals().get('optimization_time', 0.0) + self.logger.info(f"相机配置完成 - 分辨率: {actual_width}x{actual_height}, FPS: {actual_fps}") self.logger.info(f"配置耗时统计 - 缓冲区: {buffer_time:.1f}ms, 优化设置: {optimization_time:.1f}ms, 分辨率: {resolution_time:.1f}ms, 帧率: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms") self.logger.debug(f"配置详情 - 分辨率设置: {resolution_time:.1f}ms, FPS设置: {fps_time:.1f}ms, 验证: {verification_time:.1f}ms, 总计: {total_config_time:.1f}ms") @@ -852,8 +871,9 @@ class CameraManager(BaseDevice): - # 获取最新配置 - config = self.config_manager.get_device_config('camera') + # 获取最新配置(按设备名映射,已限制为 camera1/camera2) + key = self.device_name + config = self.config_manager.get_device_config(key) # 更新配置属性 self.device_index = config.get('device_index', 0) @@ -880,8 +900,8 @@ class CameraManager(BaseDevice): # 创建新队列 self.frame_cache = queue.Queue(maxsize=frame_cache_len) - # 更新设备信息 - self.device_id = f"camera_{self.device_index}" + # 更新设备信息(设备ID直接使用设备名) + self.device_id = self.device_name self.logger.info(f"相机配置重新加载成功 - 设备索引: {self.device_index}, 分辨率: {self.width}x{self.height}, FPS: {self.fps}") return True diff --git a/backend/devices/device_coordinator.py b/backend/devices/device_coordinator.py index c7cb2412..bd1d2c0a 100644 --- a/backend/devices/device_coordinator.py +++ b/backend/devices/device_coordinator.py @@ -113,10 +113,10 @@ class DeviceCoordinator: # 注册Socket.IO命名空间 self._register_namespaces() - # 初始化设备 + # 初始化设备(失败则降级继续) if not self._initialize_devices(): self.logger.warning("设备初始化失败,将以降级模式继续运行") - + # 启动监控线程 self._start_monitor() @@ -163,10 +163,12 @@ class DeviceCoordinator: future = self.executor.submit(self._init_femtobolt) futures.append(('femtobolt', future)) - # 普通相机 - if self.device_configs.get('camera', {}).get('enabled', False): - future = self.executor.submit(self._init_camera) - futures.append(('camera', future)) + # 普通相机:初始化两个实例(camera1 与 camera2) + # camera1 使用 [CAMERA1] 配置;camera2 使用 [CAMERA2](若不存在则回退为 device_index+1) + if self.device_configs.get('camera1', {}).get('enabled', True): + futures.append(('camera1', self.executor.submit(self._init_camera_by_name, 'camera1', 'CAMERA1'))) + if self.device_configs.get('camera2', {}).get('enabled', True): + futures.append(('camera2', self.executor.submit(self._init_camera_by_name, 'camera2', 'CAMERA2'))) # IMU传感器 if self.device_configs.get('imu', {}).get('enabled', False): @@ -205,21 +207,72 @@ class DeviceCoordinator: self.logger.error(f"设备初始化失败: {e}") return False - def _init_camera(self) -> bool: + def _init_camera_by_name(self, device_name: str, section: str = 'CAMERA1') -> bool: """ - 初始化普通相机 - + 按名称初始化相机,支持 camera1/camera2 并覆盖配置段 + + Args: + device_name: 设备名称(如 'camera1'、'camera2') + section: 配置段名称('CAMERA1' 或 'CAMERA2') Returns: bool: 初始化是否成功 """ try: - camera = CameraManager(self.socketio, self.config_manager) - self.devices['camera'] = camera - if camera.initialize(): + # 构造实例覆盖配置:优先读取目标配置段,否则回退到 [CAMERA1] + cfg = {} + parser = getattr(self.config_manager, 'config', None) + base_cam = self.config_manager.get_device_config('camera1') + if parser and parser.has_section(section): + # 读取所有相关键 + def get_opt(sec, key, fallback=None): + try: + return parser.get(sec, key) + except Exception: + return fallback + def get_int(sec, key, fallback=None): + try: + return parser.getint(sec, key) + except Exception: + return fallback + def get_bool(sec, key, fallback=None): + try: + return parser.getboolean(sec, key) + except Exception: + return fallback + enabled = get_bool(section, 'enabled', True) + if not enabled: + self.logger.info(f"{device_name} 未启用,跳过初始化") + return False + # 填充覆盖项 + idx2 = get_int(section, 'device_index', None) + if idx2 is not None: + cfg['device_index'] = idx2 + w = get_int(section, 'width', None) + h = get_int(section, 'height', None) + f = get_int(section, 'fps', None) + buf = get_int(section, 'buffer_size', None) + fourcc = get_opt(section, 'fourcc', None) + backend = get_opt(section, 'backend', None) + if w is not None: cfg['width'] = w + if h is not None: cfg['height'] = h + if f is not None: cfg['fps'] = f + if buf is not None: cfg['buffer_size'] = buf + if fourcc is not None: cfg['fourcc'] = fourcc + if backend is not None: cfg['backend'] = backend + else: + # section 不存在时:camera2 默认使用 device_index+1 + if device_name.lower() == 'camera2': + cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1 + else: + cfg['device_index'] = int(base_cam.get('device_index', 0)) + + camera = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg) + self.devices[device_name] = camera + if camera.initialize(): return True return False except Exception as e: - self.logger.error(f"初始化相机失败: {e}") + self.logger.error(f"初始化{device_name}失败: {e}") return False def _init_imu(self) -> bool: @@ -441,8 +494,8 @@ class DeviceCoordinator: success_count = 0 for device_name, device in self.devices.items(): try: - # 对深度相机(femtobolt)和普通相机(camera)直接调用初始化和启动推流 - if device_name in ['femtobolt', 'camera',"imu"]: + # 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控 + if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]: continue if hasattr(device, '_start_connection_monitor'): @@ -475,19 +528,9 @@ class DeviceCoordinator: success_count = 0 for device_name, device in self.devices.items(): try: - # 对深度相机(femtobolt)和普通相机(camera)直接调用停止推流 - if device_name in ['femtobolt', 'camera',"imu"]: - self.logger.info(f"停止{device_name}设备推流") - - # # 调用设备的cleanup方法清理资源,停止推流 - # if hasattr(device, 'cleanup'): - # if device.cleanup(): - # success_count += 1 - # self.logger.info(f"{device_name}设备推流已停止") - # else: - # self.logger.warning(f"{device_name}设备推流停止失败") - # else: - # self.logger.warning(f"{device_name}设备不支持推流停止") + # 对深度相机(femtobolt)和普通相机(camera1/camera2)直接跳过连接监控停止 + if device_name in ['femtobolt', 'camera1', 'camera2', "imu"]: + self.logger.info(f"停止{device_name}设备推流") continue if hasattr(device, '_stop_connection_monitor'): @@ -592,13 +635,58 @@ class DeviceCoordinator: new_device = None try: - # 根据设备类型重新创建实例 - if device_name == 'camera': + # 根据设备类型重新创建实例(仅支持 camera1/camera2) + if device_name in ('camera1', 'camera2'): try: from .camera_manager import CameraManager except ImportError: from camera_manager import CameraManager - new_device = CameraManager(self.socketio, self.config_manager) + # 为 camera1/camera2 构造实例配置 + section = 'CAMERA1' if device_name == 'camera1' else 'CAMERA2' + cfg = {} + parser = getattr(self.config_manager, 'config', None) + base_cam = self.config_manager.get_device_config('camera1') + if parser and parser.has_section(section): + def get_opt(sec, key, fallback=None): + try: + return parser.get(sec, key) + except Exception: + return fallback + def get_int(sec, key, fallback=None): + try: + return parser.getint(sec, key) + except Exception: + return fallback + def get_bool(sec, key, fallback=None): + try: + return parser.getboolean(sec, key) + except Exception: + return fallback + enabled = get_bool(section, 'enabled', True) + if not enabled: + raise Exception(f"{device_name} 未启用") + idx2 = get_int(section, 'device_index', None) + if idx2 is not None: + cfg['device_index'] = idx2 + w = get_int(section, 'width', None) + h = get_int(section, 'height', None) + f = get_int(section, 'fps', None) + buf = get_int(section, 'buffer_size', None) + fourcc = get_opt(section, 'fourcc', None) + backend = get_opt(section, 'backend', None) + if w is not None: cfg['width'] = w + if h is not None: cfg['height'] = h + if f is not None: cfg['fps'] = f + if buf is not None: cfg['buffer_size'] = buf + if fourcc is not None: cfg['fourcc'] = fourcc + if backend is not None: cfg['backend'] = backend + else: + # section 不存在时:camera2 默认使用 [CAMERA1] 的 device_index + 1 + if device_name == 'camera2': + cfg['device_index'] = int(base_cam.get('device_index', 0)) + 1 + else: + cfg['device_index'] = int(base_cam.get('device_index', 0)) + new_device = CameraManager(self.socketio, self.config_manager, device_name=device_name, instance_config=cfg) elif device_name == 'imu': try: from .imu_manager import IMUManager @@ -812,7 +900,12 @@ class DeviceCoordinator: self.executor.shutdown(wait=True) # 清理Socket管理器 - self.socket_manager.cleanup() + try: + self.socket_manager.cleanup_all() + except Exception: + # 兼容旧接口 + if hasattr(self.socket_manager, 'cleanup'): + self.socket_manager.cleanup() self.logger.info("设备协调器已关闭") @@ -837,7 +930,7 @@ def test_restart_device(device_name=None): Args: device_name (str, optional): 指定要测试的设备名称。如果为None,则自动选择第一个可用设备。 - 可选值: 'camera', 'imu', 'pressure', 'femtobolt' + 可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt' """ import time import threading @@ -847,22 +940,13 @@ def test_restart_device(device_name=None): print("设备协调器重启功能测试") print("=" * 60) - # 创建模拟的SocketIO和配置管理器 + # 创建模拟的SocketIO(使用真实配置文件) mock_socketio = Mock() - mock_config_manager = Mock() - - # 模拟配置数据 - mock_config_manager.get_device_config.return_value = { - 'camera': {'enabled': True, 'device_id': 0, 'fps': 30}, - 'imu': {'enabled': True, 'device_type': 'mock'}, - 'pressure': {'enabled': True, 'device_type': 'mock'}, - 'femtobolt': {'enabled': False} - } try: # 创建设备协调器实例 print("1. 创建设备协调器...") - coordinator = DeviceCoordinator(mock_socketio, mock_config_manager) + coordinator = DeviceCoordinator(mock_socketio) # 初始化设备协调器 print("2. 初始化设备协调器...") @@ -882,13 +966,14 @@ def test_restart_device(device_name=None): print("❌ 没有可用的设备进行测试") return False - # 根据参数选择测试设备 + # 根据参数选择测试设备(仅支持 camera1/camera2/imu/pressure/femtobolt) if device_name: - if device_name in available_devices: + allowed = {'camera1', 'camera2', 'imu', 'pressure', 'femtobolt'} + if device_name in available_devices and device_name in allowed: test_device = device_name print(f"3. 使用指定的测试设备: {test_device}") else: - print(f"❌ 指定的设备 '{device_name}' 不存在") + print(f"❌ 指定的设备 '{device_name}' 不存在或不受支持") print(f" 可用设备: {available_devices}") return False else: @@ -992,7 +1077,7 @@ if __name__ == "__main__": ) # 执行测试 - # 可选值: 'camera', 'imu', 'pressure', 'femtobolt' + # 可选值: 'camera1', 'camera2', 'imu', 'pressure', 'femtobolt' success = test_restart_device('pressure') if success: diff --git a/backend/devices/screen_recorder.py b/backend/devices/screen_recorder.py index fa91b9f0..161bdd8c 100644 --- a/backend/devices/screen_recorder.py +++ b/backend/devices/screen_recorder.py @@ -242,7 +242,7 @@ class RecordingManager: return frame - def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]: + def start_recording(self, session_id: str, patient_id: str, screen_location: List[int], camera1_location: List[int], camera2_location: List[int], femtobolt_location: List[int], recording_types: List[str] = None) -> Dict[str, Any]: """ 启动同步录制 @@ -250,9 +250,10 @@ class RecordingManager: session_id: 检测会话ID patient_id: 患者ID screen_location: 屏幕录制区域 [x, y, w, h] - camera_location: 相机录制区域 [x, y, w, h] + camera1_location: 相机1录制区域 [x, y, w, h] + camera2_location: 相机2录制区域 [x, y, w, h] femtobolt_location: FemtoBolt录制区域 [x, y, w, h] - recording_types: 录制类型列表 ['screen', 'feet', 'femtobolt'],默认全部录制 + recording_types: 录制类型列表 ['screen', 'camera1', 'camera2', 'feet', 'femtobolt'],默认全部录制 Returns: Dict: 录制启动状态和信息 @@ -263,6 +264,8 @@ class RecordingManager: 'patient_id': patient_id, 'recording_start_time': None, 'video_paths': { + 'camera1_video': None, + 'camera2_video': None, 'feet_video': None, 'screen_video': None, 'femtobolt_video': None @@ -277,7 +280,7 @@ class RecordingManager: return result # 设置默认录制类型 - recording_types = ['screen', 'feet'] + recording_types = ['screen', 'camera1' ,'camera2'] # 验证录制区域参数(仅对启用的录制类型进行验证) if 'screen' in recording_types: @@ -286,10 +289,16 @@ class RecordingManager: result['message'] = '屏幕录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]' return result - if 'feet' in recording_types: - if not camera_location or not isinstance(camera_location, list) or len(camera_location) != 4: + if 'camera1' in recording_types: + if not camera1_location or not isinstance(camera1_location, list) or len(camera1_location) != 4: result['success'] = False - result['message'] = '相机录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]' + result['message'] = '相机1录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]' + return result + + if 'camera2' in recording_types: + if not camera2_location or not isinstance(camera2_location, list) or len(camera2_location) != 4: + result['success'] = False + result['message'] = '相机2录制区域参数无效或缺失,必须是包含4个元素的数组[x, y, w, h]' return result if 'femtobolt' in recording_types: @@ -303,19 +312,22 @@ class RecordingManager: # self.logger.info(f'检测sessionID................: {self.current_session_id}') self.current_patient_id = patient_id self.screen_region = tuple(screen_location) # [x, y, w, h] -> (x, y, w, h) - self.camera_region = tuple(camera_location) # [x, y, w, h] -> (x, y, w, h) + self.camera1_region = tuple(camera1_location) # [x, y, w, h] -> (x, y, w, h) + self.camera2_region = tuple(camera2_location) # [x, y, w, h] -> (x, y, w, h) self.femtobolt_region = tuple(femtobolt_location) # [x, y, w, h] -> (x, y, w, h) # 根据录制区域大小设置自适应帧率 if 'screen' in recording_types: self._set_adaptive_fps_by_region('screen', self.screen_region) - if 'feet' in recording_types: - self._set_adaptive_fps_by_region('camera', self.camera_region) + if 'camera1' in recording_types: + self._set_adaptive_fps_by_region('camera1', self.camera1_region) + if 'camera2' in recording_types: + self._set_adaptive_fps_by_region('camera2', self.camera2_region) if 'femtobolt' in recording_types: self._set_adaptive_fps_by_region('femtobolt', self.femtobolt_region) # 设置录制同步 - active_recording_count = len([t for t in recording_types if t in ['screen', 'feet', 'femtobolt']]) + active_recording_count = len([t for t in recording_types if t in ['screen', 'camera1', 'camera2', 'femtobolt']]) self.recording_sync_barrier = threading.Barrier(active_recording_count) self.recording_start_sync.clear() self.global_recording_start_time = None @@ -340,7 +352,8 @@ class RecordingManager: return result - feet_video_path = os.path.join(base_path, 'feet.mp4') + camera1_video_path = os.path.join(base_path, 'camera1.mp4') + camera2_video_path = os.path.join(base_path, 'camera2.mp4') screen_video_path = os.path.join(base_path, 'screen.mp4') femtobolt_video_path = os.path.join(base_path, 'femtobolt.mp4') @@ -350,7 +363,8 @@ class RecordingManager: 'session_id': session_id, 'status': 'recording', 'video_paths': { - 'normal_video_path': os.path.join(db_base_path, 'feet.mp4'), + 'camera1_video_path': os.path.join(db_base_path, 'camera1.mp4'), + 'camera2_video_path': os.path.join(db_base_path, 'camera2.mp4'), 'screen_video_path': os.path.join(db_base_path, 'screen.mp4'), 'femtobolt_video_path': os.path.join(db_base_path, 'femtobolt.mp4') } @@ -373,7 +387,20 @@ class RecordingManager: # 根据录制类型选择性地初始化视频写入器,使用各自的自适应帧率 self.screen_video_writer = None self.femtobolt_video_writer = None - self.feet_video_writer = None + self.camera1_video_writer = None + self.camera2_video_writer = None + + if 'camera1' in recording_types: + self.camera1_video_writer = cv2.VideoWriter( + camera1_video_path, fourcc, self.camera1_current_fps, (self.camera1_region[2], self.camera1_region[3]) + ) + self.logger.info(f'相机1视频写入器使用帧率: {self.camera1_current_fps}fps') + + if 'camera2' in recording_types: + self.camera2_video_writer = cv2.VideoWriter( + camera2_video_path, fourcc, self.camera2_current_fps, (self.camera2_region[2], self.camera2_region[3]) + ) + self.logger.info(f'相机2视频写入器使用帧率: {self.camera2_current_fps}fps') if 'screen' in recording_types: self.screen_video_writer = cv2.VideoWriter( @@ -386,23 +413,24 @@ class RecordingManager: femtobolt_video_path, fourcc, self.femtobolt_current_fps, (self.femtobolt_region[2], self.femtobolt_region[3]) ) self.logger.info(f'FemtoBolt视频写入器使用帧率: {self.femtobolt_current_fps}fps') - - if 'feet' in recording_types: - self.feet_video_writer = cv2.VideoWriter( - feet_video_path, fourcc, self.camera_current_fps, (self.camera_region[2], self.camera_region[3]) - ) - self.logger.info(f'足部视频写入器使用帧率: {self.camera_current_fps}fps') - - # 检查视频写入器状态(仅检查启用的录制类型) - # 检查足部视频写入器 - if 'feet' in recording_types: - if self.feet_video_writer and self.feet_video_writer.isOpened(): - self.logger.info(f'足部视频写入器初始化成功: {feet_video_path}') + + + # 检查相机1视频写入器 + if 'camera1' in recording_types: + if self.camera1_video_writer and self.camera1_video_writer.isOpened(): + self.logger.info(f'相机1视频写入器初始化成功: {camera1_video_path}') else: - self.logger.error(f'足部视频写入器初始化失败: {feet_video_path}') + self.logger.error(f'相机1视频写入器初始化失败: {camera1_video_path}') + else: + self.logger.info('相机1录制功能已禁用') + # 检查相机2视频写入器 + if 'camera2' in recording_types: + if self.camera2_video_writer and self.camera2_video_writer.isOpened(): + self.logger.info(f'相机2视频写入器初始化成功: {camera2_video_path}') + else: + self.logger.error(f'相机2视频写入器初始化失败: {camera2_video_path}') else: self.logger.info('足部录制功能已禁用') - # 检查屏幕视频写入器 if 'screen' in recording_types: if self.screen_video_writer and self.screen_video_writer.isOpened(): @@ -426,15 +454,25 @@ class RecordingManager: self.sync_recording = True # 根据录制类型启动对应的录制线程 - if 'feet' in recording_types and self.feet_video_writer and self.feet_video_writer.isOpened(): - self.feet_recording_thread = threading.Thread( + if 'camera1' in recording_types and self.camera1_video_writer and self.camera1_video_writer.isOpened(): + self.camera1_recording_thread = threading.Thread( target=self._generic_recording_thread, - args=('camera', self.camera_region, feet_video_path, self.feet_video_writer), + args=('camera1', self.camera1_region, camera1_video_path, self.camera1_video_writer), daemon=True, - name='FeetRecordingThread' + name='Camera1RecordingThread' ) - self.feet_recording_thread.start() - # self.logger.info(f'足部录制线程已启动 - 区域: {self.camera_region}, 输出文件: {feet_video_path}') + self.camera1_recording_thread.start() + # self.logger.info(f'相机1录制线程已启动 - 区域: {self.camera1_region}, 输出文件: {camera1_video_path}') + + if 'camera2' in recording_types and self.camera2_video_writer and self.camera2_video_writer.isOpened(): + self.camera2_recording_thread = threading.Thread( + target=self._generic_recording_thread, + args=('camera2', self.camera2_region, camera2_video_path, self.camera2_video_writer), + daemon=True, + name='Camera2RecordingThread' + ) + self.camera2_recording_thread.start() + # self.logger.info(f'相机2录制线程已启动 - 区域: {self.camera2_region}, 输出文件: {camera2_video_path}') if 'screen' in recording_types and self.screen_video_writer and self.screen_video_writer.isOpened(): self.screen_recording_thread = threading.Thread( @@ -506,8 +544,10 @@ class RecordingManager: # 收集活跃的录制线程 active_threads = [] - if hasattr(self, 'feet_recording_thread') and self.feet_recording_thread and self.feet_recording_thread.is_alive(): - active_threads.append(('feet', self.feet_recording_thread)) + if hasattr(self, 'camera1_recording_thread') and self.camera1_recording_thread and self.camera1_recording_thread.is_alive(): + active_threads.append(('camera1', self.camera1_recording_thread)) + if hasattr(self, 'camera2_recording_thread') and self.camera2_recording_thread and self.camera2_recording_thread.is_alive(): + active_threads.append(('camera2', self.camera2_recording_thread)) if hasattr(self, 'screen_recording_thread') and self.screen_recording_thread and self.screen_recording_thread.is_alive(): active_threads.append(('screen', self.screen_recording_thread)) if hasattr(self, 'femtobolt_recording_thread') and self.femtobolt_recording_thread and self.femtobolt_recording_thread.is_alive(): @@ -535,9 +575,12 @@ class RecordingManager: if thread_name == 'screen': expected_frames = int(actual_recording_duration * self.screen_current_fps) self.logger.info(f' 屏幕录制预期帧数: {expected_frames}帧 (帧率{self.screen_current_fps}fps)') - elif thread_name == 'feet': + elif thread_name == 'camera1': expected_frames = int(actual_recording_duration * self.camera_current_fps) - self.logger.info(f' 足部录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)') + self.logger.info(f' 相机1录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)') + elif thread_name == 'camera2': + expected_frames = int(actual_recording_duration * self.camera_current_fps) + self.logger.info(f' 相机2录制预期帧数: {expected_frames}帧 (帧率{self.camera_current_fps}fps)') elif thread_name == 'femtobolt': expected_frames = int(actual_recording_duration * self.femtobolt_current_fps) self.logger.info(f' FemtoBolt录制预期帧数: {expected_frames}帧 (帧率{self.femtobolt_current_fps}fps)') @@ -575,7 +618,7 @@ class RecordingManager: 通用录制线程,支持屏幕、相机和FemtoBolt录制 Args: - recording_type: 录制类型 ('screen', 'camera', 'femtobolt') + recording_type: 录制类型 ('screen', 'camera1', 'camera2', 'femtobolt') region: 录制区域 (x, y, width, height) output_file_name: 输出文件名 video_writer: 视频写入器对象 @@ -587,8 +630,10 @@ class RecordingManager: # 根据录制类型获取对应的自适应帧率 if recording_type == 'screen': target_fps = self.screen_current_fps - elif recording_type == 'camera': - target_fps = self.camera_current_fps + elif recording_type == 'camera1': + target_fps = self.camera1_current_fps + elif recording_type == 'camera2': + target_fps = self.camera2_current_fps elif recording_type == 'femtobolt': target_fps = self.femtobolt_current_fps else: @@ -789,9 +834,9 @@ class RecordingManager: - def collect_detection_data(self, session_id: str, patient_id: str, detection_data: Dict[str, Any]) -> Dict[str, Any]: + def save_detection_images(self, session_id: str, patient_id: str, detection_data: Dict[str, Any]) -> Dict[str, Any]: """ - 保存前端传入的检测数据和图片 + 保存前端传入的检测图片到指定目录 Args: session_id: 检测会话ID @@ -824,7 +869,8 @@ class RecordingManager: 'body_image': None, 'foot_data': detection_data.get('foot_data'), 'foot_data_image': None, - 'foot_image': None, + 'foot1_image': None, + 'foot2_image': None, 'screen_image': None, 'timestamp': timestamp } @@ -833,7 +879,8 @@ class RecordingManager: # 保存图片数据 image_fields = [ ('body_image', 'body'), - ('foot_image', 'foot'), + ('foot1_image', 'foot1'), + ('foot2_image', 'foot2'), ('foot_data_image', 'foot_data') ] diff --git a/backend/devices/test/devicetest.py b/backend/devices/test/devicetest.py index 850050e4..56617410 100644 --- a/backend/devices/test/devicetest.py +++ b/backend/devices/test/devicetest.py @@ -84,7 +84,8 @@ class DeviceTestServer: # 设备管理器和模拟数据生成器 self.device_managers = { - 'camera': CameraManager(self.socketio, self.config_manager), + 'camera1': CameraManager(self.socketio, self.config_manager, device_name='camera1'), + 'camera2': CameraManager(self.socketio, self.config_manager, device_name='camera2'), 'femtobolt': FemtoBoltManager(self.socketio, self.config_manager), 'imu': IMUManager(self.socketio, self.config_manager), 'pressure': PressureManager(self.socketio, self.config_manager) @@ -340,7 +341,8 @@ class DeviceTestServer: def _get_event_name(self, device_name: str) -> str: """获取设备对应的事件名称""" event_map = { - 'camera': 'camera_frame', + 'camera1': 'camera_frame', + 'camera2': 'camera_frame', 'femtobolt': 'femtobolt_frame', 'imu': 'imu_data', 'pressure': 'pressure_data' diff --git a/backend/devices/test_config_api.py b/backend/devices/test_config_api.py deleted file mode 100644 index 194b3205..00000000 --- a/backend/devices/test_config_api.py +++ /dev/null @@ -1,227 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -配置API测试脚本 -用于测试设备配置HTTP API的功能 -""" - -import requests -import json -import time - - -class ConfigAPITester: - """配置API测试器""" - - def __init__(self, base_url="http://localhost:5002"): - """ - 初始化测试器 - - Args: - base_url: API基础URL - """ - self.base_url = base_url - self.api_url = f"{base_url}/api/config" - - def test_get_all_configs(self): - """测试获取所有设备配置""" - print("\n=== 测试获取所有设备配置 ===") - try: - response = requests.get(f"{self.api_url}/devices") - result = response.json() - - if result['success']: - print("✓ 获取所有设备配置成功") - print(json.dumps(result['data'], indent=2, ensure_ascii=False)) - else: - print(f"✗ 获取失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def test_get_single_config(self, device_name): - """测试获取单个设备配置""" - print(f"\n=== 测试获取{device_name}设备配置 ===") - try: - response = requests.get(f"{self.api_url}/devices/{device_name}") - result = response.json() - - if result['success']: - print(f"✓ 获取{device_name}配置成功") - print(json.dumps(result['data'], indent=2, ensure_ascii=False)) - else: - print(f"✗ 获取失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def test_set_imu_config(self): - """测试设置IMU配置""" - print("\n=== 测试设置IMU配置 ===") - try: - data = { - "device_type": "real", - "port": "COM6", - "baudrate": 9600 - } - - response = requests.post(f"{self.api_url}/devices/imu", json=data) - result = response.json() - - if result['success']: - print("✓ 设置IMU配置成功") - print(f"消息: {result['message']}") - print("更新后的配置:") - print(json.dumps(result['config'], indent=2, ensure_ascii=False)) - else: - print(f"✗ 设置失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def test_set_pressure_config(self): - """测试设置压力板配置""" - print("\n=== 测试设置压力板配置 ===") - try: - data = { - "device_type": "real", - "use_mock": False, - "port": "COM5", - "baudrate": 115200 - } - - response = requests.post(f"{self.api_url}/devices/pressure", json=data) - result = response.json() - - if result['success']: - print("✓ 设置压力板配置成功") - print(f"消息: {result['message']}") - print("更新后的配置:") - print(json.dumps(result['config'], indent=2, ensure_ascii=False)) - else: - print(f"✗ 设置失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def test_set_camera_config(self): - """测试设置相机配置""" - print("\n=== 测试设置相机配置 ===") - try: - data = { - "device_index": 0, - "width": 1280, - "height": 720, - "fps": 30 - } - - response = requests.post(f"{self.api_url}/devices/camera", json=data) - result = response.json() - - if result['success']: - print("✓ 设置相机配置成功") - print(f"消息: {result['message']}") - print("更新后的配置:") - print(json.dumps(result['config'], indent=2, ensure_ascii=False)) - else: - print(f"✗ 设置失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def test_set_femtobolt_config(self): - """测试设置FemtoBolt配置""" - print("\n=== 测试设置FemtoBolt配置 ===") - try: - data = { - "color_resolution": "1080P", - "depth_mode": "NFOV_UNBINNED", - "fps": 30, - "depth_range_min": 1200, - "depth_range_max": 1500 - } - - response = requests.post(f"{self.api_url}/devices/femtobolt", json=data) - result = response.json() - - if result['success']: - print("✓ 设置FemtoBolt配置成功") - print(f"消息: {result['message']}") - print("更新后的配置:") - print(json.dumps(result['config'], indent=2, ensure_ascii=False)) - else: - print(f"✗ 设置失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def test_validate_config(self): - """测试验证配置""" - print("\n=== 测试验证配置 ===") - try: - response = requests.get(f"{self.api_url}/validate") - result = response.json() - - if result['success']: - print("✓ 配置验证成功") - validation_result = result['data'] - print(f"配置有效性: {validation_result['valid']}") - if validation_result['errors']: - print(f"错误: {validation_result['errors']}") - if validation_result['warnings']: - print(f"警告: {validation_result['warnings']}") - else: - print(f"✗ 验证失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def test_reload_config(self): - """测试重新加载配置""" - print("\n=== 测试重新加载配置 ===") - try: - response = requests.post(f"{self.api_url}/reload") - result = response.json() - - if result['success']: - print("✓ 重新加载配置成功") - print(f"消息: {result['message']}") - else: - print(f"✗ 重新加载失败: {result['message']}") - - except Exception as e: - print(f"✗ 请求异常: {e}") - - def run_all_tests(self): - """运行所有测试""" - print("开始配置API功能测试...") - print(f"API地址: {self.api_url}") - - # 等待服务启动 - print("\n等待API服务启动...") - time.sleep(2) - - # 运行测试 - self.test_get_all_configs() - - # 测试获取单个设备配置 - for device in ['imu', 'pressure', 'camera', 'femtobolt']: - self.test_get_single_config(device) - - # 测试设置配置 - self.test_set_imu_config() - self.test_set_pressure_config() - self.test_set_camera_config() - self.test_set_femtobolt_config() - - # 测试其他功能 - self.test_validate_config() - self.test_reload_config() - - print("\n=== 测试完成 ===") - - -if __name__ == "__main__": - # 创建测试器并运行测试 - tester = ConfigAPITester() - tester.run_all_tests() \ No newline at end of file diff --git a/backend/devices/utils/config_manager.py b/backend/devices/utils/config_manager.py index a5035e1c..5870e223 100644 --- a/backend/devices/utils/config_manager.py +++ b/backend/devices/utils/config_manager.py @@ -102,8 +102,8 @@ class ConfigManager: 'pressure_baudrate': '115200' } - # 默认相机配置 - self.config['CAMERA'] = { + # 默认相机1配置 + self.config['CAMERA1'] = { 'device_index': '0', 'width': '1280', 'height': '720', @@ -111,6 +111,15 @@ class ConfigManager: 'backend': 'directshow' } + # 默认相机2配置 + self.config['CAMERA2'] = { + 'device_index': '1', + 'width': '1280', + 'height': '720', + 'fps': '30', + 'backend': 'directshow' + } + # 默认FemtoBolt配置 self.config['FEMTOBOLT'] = { 'color_resolution': '1080P', @@ -134,7 +143,7 @@ class ConfigManager: 获取设备配置 Args: - device_name: 设备名称 (camera, femtobolt, imu, pressure) + device_name: 设备名称 (camera1, camera2, femtobolt, imu, pressure) Returns: Dict[str, Any]: 设备配置字典 @@ -144,8 +153,10 @@ class ConfigManager: config = {} - if device_name == 'camera': - config = self._get_camera_config() + if device_name == 'camera1': + config = self._get_camera1_config() + elif device_name == 'camera2': + config = self._get_camera2_config() elif device_name == 'femtobolt': config = self._get_femtobolt_config() elif device_name == 'imu': @@ -159,7 +170,7 @@ class ConfigManager: self._device_configs[device_name] = config return config.copy() - def _get_camera_config(self) -> Dict[str, Any]: + def _get_camera1_config(self) -> Dict[str, Any]: """ 获取相机配置 @@ -167,16 +178,32 @@ class ConfigManager: Dict[str, Any]: 相机配置 """ return { - 'enabled': self.config.getboolean('CAMERA', 'enabled', fallback=True), - 'device_index': self.config.getint('CAMERA', 'device_index', fallback=0), - 'width': self.config.getint('CAMERA', 'width', fallback=1280), - 'height': self.config.getint('CAMERA', 'height', fallback=720), - 'fps': self.config.getint('CAMERA', 'fps', fallback=30), - 'buffer_size': self.config.getint('CAMERA', 'buffer_size', fallback=1), - 'fourcc': self.config.get('CAMERA', 'fourcc', fallback='MJPG'), - 'backend': self.config.get('CAMERA', 'backend', fallback='directshow') + 'enabled': self.config.getboolean('CAMERA1', 'enabled', fallback=True), + 'device_index': self.config.getint('CAMERA1', 'device_index', fallback=0), + 'width': self.config.getint('CAMERA1', 'width', fallback=1280), + 'height': self.config.getint('CAMERA1', 'height', fallback=720), + 'fps': self.config.getint('CAMERA1', 'fps', fallback=30), + 'buffer_size': self.config.getint('CAMERA1', 'buffer_size', fallback=1), + 'fourcc': self.config.get('CAMERA1', 'fourcc', fallback='MJPG'), + 'backend': self.config.get('CAMERA1', 'backend', fallback='directshow') } + def _get_camera2_config(self) -> Dict[str, Any]: + """ + 获取相机配置 + Returns: + Dict[str, Any]: + """ + return { + 'enabled': self.config.getboolean('CAMERA2', 'enabled', fallback=True), + 'device_index': self.config.getint('CAMERA2', 'device_index', fallback=0), + 'width': self.config.getint('CAMERA2', 'width', fallback=1280), + 'height': self.config.getint('CAMERA2', 'height', fallback=720), + 'fps': self.config.getint('CAMERA2', 'fps', fallback=30), + 'buffer_size': self.config.getint('CAMERA2', 'buffer_size', fallback=1), + 'fourcc': self.config.get('CAMERA2', 'fourcc', fallback='MJPG'), + 'backend': self.config.get('CAMERA2', 'backend', fallback='directshow') + } def _get_femtobolt_config(self) -> Dict[str, Any]: """ 获取FemtoBolt配置 @@ -332,7 +359,7 @@ class ConfigManager: warnings = [] # 验证必需的配置段 - required_sections = ['DEVICES', 'CAMERA', 'FEMTOBOLT', 'SYSTEM'] + required_sections = ['DEVICES', 'CAMERA1', 'CAMERA2', 'FEMTOBOLT', 'SYSTEM'] for section in required_sections: if not self.config.has_section(section): errors.append(f"缺少必需的配置段: {section}") @@ -438,7 +465,7 @@ class ConfigManager: 'message': f'设置压力板配置失败: {str(e)}' } - def set_camera_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: + def set_camera1_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: """ 设置相机设备配置 @@ -457,15 +484,15 @@ class ConfigManager: try: # 验证必需参数 if 'device_index' in config_data: - self.set_config_value('CAMERA', 'device_index', str(config_data['device_index'])) + self.set_config_value('CAMERA1', 'device_index', str(config_data['device_index'])) if 'width' in config_data: - self.set_config_value('CAMERA', 'width', str(config_data['width'])) + self.set_config_value('CAMERA1', 'width', str(config_data['width'])) if 'height' in config_data: - self.set_config_value('CAMERA', 'height', str(config_data['height'])) + self.set_config_value('CAMERA1', 'height', str(config_data['height'])) if 'fps' in config_data: - self.set_config_value('CAMERA', 'fps', str(config_data['fps'])) + self.set_config_value('CAMERA1', 'fps', str(config_data['fps'])) if 'backend' in config_data: - self.set_config_value('CAMERA', 'backend', str(config_data['backend'])) + self.set_config_value('CAMERA1', 'backend', str(config_data['backend'])) # 保存配置 self.save_config() @@ -474,7 +501,7 @@ class ConfigManager: return { 'success': True, 'message': '相机配置更新成功', - 'config': self.get_device_config('camera') + 'config': self.get_device_config('camera1') } except Exception as e: self.logger.error(f"设置相机配置失败: {e}") @@ -482,7 +509,50 @@ class ConfigManager: 'success': False, 'message': f'设置相机配置失败: {str(e)}' } + def set_camera2_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: + """ + 设置相机设备配置 + + Args: + config_data: 相机配置数据 + { + 'device_index': 1, + 'width': 1280, + 'height': 720, + 'fps': 30 + } + Returns: + Dict[str, Any]: 设置结果 + """ + try: + # 验证必需参数 + if 'device_index' in config_data: + self.set_config_value('CAMERA2', 'device_index', str(config_data['device_index'])) + if 'width' in config_data: + self.set_config_value('CAMERA2', 'width', str(config_data['width'])) + if 'height' in config_data: + self.set_config_value('CAMERA2', 'height', str(config_data['height'])) + if 'fps' in config_data: + self.set_config_value('CAMERA2', 'fps', str(config_data['fps'])) + if 'backend' in config_data: + self.set_config_value('CAMERA2', 'backend', str(config_data['backend'])) + + # 保存配置 + self.save_config() + + self.logger.info(f"相机配置已更新: {config_data}") + return { + 'success': True, + 'message': '相机配置更新成功', + 'config': self.get_device_config('camera2') + } + except Exception as e: + self.logger.error(f"设置相机配置失败: {e}") + return { + 'success': False, + 'message': f'设置相机配置失败: {str(e)}' + } def set_femtobolt_config(self, config_data: Dict[str, Any]) -> Dict[str, Any]: """ 设置FemtoBolt设备配置 @@ -541,7 +611,8 @@ class ConfigManager: return { 'imu': self.get_device_config('imu'), 'pressure': self.get_device_config('pressure'), - 'camera': self.get_device_config('camera'), + 'camera1': self.get_device_config('camera1'), + 'camera2': self.get_device_config('camera2'), 'femtobolt': self.get_device_config('femtobolt') } @@ -610,27 +681,27 @@ class ConfigManager: self.logger.error(error_msg) # 相机配置 - if 'camera' in configs: + if 'camera1' in configs: try: - config_data = configs['camera'] + config_data = configs['camera1'] if 'device_index' in config_data: - self.set_config_value('CAMERA', 'device_index', str(config_data['device_index'])) + self.set_config_value('CAMERA1', 'device_index', str(config_data['device_index'])) if 'width' in config_data: - self.set_config_value('CAMERA', 'width', str(config_data['width'])) + self.set_config_value('CAMERA1', 'width', str(config_data['width'])) if 'height' in config_data: - self.set_config_value('CAMERA', 'height', str(config_data['height'])) + self.set_config_value('CAMERA1', 'height', str(config_data['height'])) if 'fps' in config_data: - self.set_config_value('CAMERA', 'fps', str(config_data['fps'])) + self.set_config_value('CAMERA1', 'fps', str(config_data['fps'])) if 'buffer_size' in config_data: - self.set_config_value('CAMERA', 'buffer_size', str(config_data['buffer_size'])) + self.set_config_value('CAMERA1', 'buffer_size', str(config_data['buffer_size'])) if 'fourcc' in config_data: - self.set_config_value('CAMERA', 'fourcc', config_data['fourcc']) + self.set_config_value('CAMERA1', 'fourcc', config_data['fourcc']) if 'tx_max_width' in config_data: - self.set_config_value('CAMERA', 'tx_max_width', str(config_data['tx_max_width'])) + self.set_config_value('CAMERA1', 'tx_max_width', str(config_data['tx_max_width'])) if 'backend' in config_data: - self.set_config_value('CAMERA', 'backend', str(config_data['backend'])) + self.set_config_value('CAMERA1', 'backend', str(config_data['backend'])) - results['camera'] = { + results['camera1'] = { 'success': True, 'message': '相机配置更新成功', 'config': config_data @@ -638,10 +709,42 @@ class ConfigManager: self.logger.info(f"相机配置已更新: {config_data}") except Exception as e: error_msg = f'设置相机配置失败: {str(e)}' - results['camera'] = {'success': False, 'message': error_msg} + results['camera1'] = {'success': False, 'message': error_msg} errors.append(f"相机: {error_msg}") self.logger.error(error_msg) + if 'camera2' in configs: + try: + config_data = configs['camera2'] + if 'device_index' in config_data: + self.set_config_value('CAMERA2', 'device_index', str(config_data['device_index'])) + if 'width' in config_data: + self.set_config_value('CAMERA2', 'width', str(config_data['width'])) + if 'height' in config_data: + self.set_config_value('CAMERA2', 'height', str(config_data['height'])) + if 'fps' in config_data: + self.set_config_value('CAMERA2', 'fps', str(config_data['fps'])) + if 'buffer_size' in config_data: + self.set_config_value('CAMERA2', 'buffer_size', str(config_data['buffer_size'])) + if 'fourcc' in config_data: + self.set_config_value('CAMERA2', 'fourcc', config_data['fourcc']) + if 'tx_max_width' in config_data: + self.set_config_value('CAMERA2', 'tx_max_width', str(config_data['tx_max_width'])) + if 'backend' in config_data: + self.set_config_value('CAMERA2', 'backend', str(config_data['backend'])) + + results['camera2'] = { + 'success': True, + 'message': '相机配置更新成功', + 'config': config_data + } + self.logger.info(f"相机配置已更新: {config_data}") + except Exception as e: + error_msg = f'设置相机配置失败: {str(e)}' + results['camera2'] = {'success': False, 'message': error_msg} + errors.append(f"相机2: {error_msg}") + self.logger.error(error_msg) + # FemtoBolt配置 if 'femtobolt' in configs: try: @@ -703,7 +806,8 @@ class ConfigManager: { 'imu': {'device_type': 'real', 'port': 'COM7', 'baudrate': 9600}, 'pressure': {'device_type': 'real', 'port': 'COM8', 'baudrate': 115200}, - 'camera': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30}, + 'camera1': {'device_index': 0, 'width': 1280, 'height': 720, 'fps': 30}, + 'camera2': {'device_index': 1, 'width': 1280, 'height': 720, 'fps': 30}, 'femtobolt': {'color_resolution': '1080P', 'depth_mode': 'NFOV_UNBINNED', 'fps': 15} } diff --git a/backend/main.py b/backend/main.py index 2d5f5467..bbfa72ec 100644 --- a/backend/main.py +++ b/backend/main.py @@ -25,6 +25,7 @@ sys.path.append(os.path.dirname(os.path.abspath(__file__))) # 导入模块 from database import DatabaseManager from utils import config as app_config +from utils import DataValidator # 添加数据验证器导入 from devices.camera_manager import CameraManager from devices.imu_manager import IMUManager from devices.pressure_manager import PressureManager @@ -163,8 +164,9 @@ class AppServer: config_path = os.path.join(os.path.dirname(__file__), 'config.ini') self.config.read(config_path, encoding='utf-8') - device_index = self.config.get('CAMERA', 'device_index', fallback=None) - print(f"设备号: {device_index}") + camera1_index = self.config.get('CAMERA1', 'device_index', fallback=None) + camera2_index = self.config.get('CAMERA2', 'device_index', fallback=None) + print(f"相机1设备号: {camera1_index}, 相机2设备号: {camera2_index}") def init_app(self): """初始化应用组件""" @@ -243,16 +245,21 @@ class AppServer: # 初始化录制管理器 self.logger.info('正在初始化录制管理器...') - camera_manager = self.device_managers.get('camera') - if camera_manager: - self.recording_manager = RecordingManager( - camera_manager=camera_manager, - db_manager=self.db_manager - ) - self.logger.info('录制管理器初始化完成') - else: - self.recording_manager = None - self.logger.warning('相机设备未初始化,录制管理器将不可用') + camera1_manager = self.device_managers.get('camera1') + camera2_manager = self.device_managers.get('camera2') + femtobolt_manager = self.device_managers.get('femtobolt') + pressure_manager = self.device_managers.get('pressure') + + # 录制管理器当前采用屏幕区域截取方式进行相机录制,不依赖 CameraManager + # 但保留其他设备管理器以便后续扩展(如FemtoBolt、压力传感器) + self.recording_manager = RecordingManager( + camera_manager=None, + db_manager=self.db_manager, + femtobolt_manager=femtobolt_manager, + pressure_manager=pressure_manager, + config_manager=self.config_manager + ) + self.logger.info('录制管理器初始化完成') # 启动Flask应用 host = self.host @@ -895,15 +902,16 @@ class AppServer: if status not in ['approved', 'rejected']: return jsonify({'success': False, 'error': '无效的审核状态'}), 400 - result = self.db_manager.update_user_status(user_id, status) - if result: + # 使用数据库层已有的审核方法 + try: + self.db_manager.approve_user(user_id, approved=(status == 'approved')) return jsonify({ 'success': True, 'message': f'用户已{"通过" if status == "approved" else "拒绝"}审核' }) - else: - return jsonify({'success': False, 'error': '用户不存在'}), 404 - + except Exception: + return jsonify({'success': False, 'error': '用户不存在或审核失败'}), 404 + except Exception as e: self.logger.error(f'审核用户失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 @@ -952,48 +960,52 @@ class AppServer: return jsonify({'success': False, 'error': str(e)}), 500 elif flask_request.method == 'POST': - # 创建患者 + # 创建新患者 try: - # 检查Content-Type - if not flask_request.is_json: - return jsonify({'success': False, 'message': '请求Content-Type必须为application/json'}), 415 + data = flask_request.get_json() - data = flask_request.get_json(force=True) - - required_fields = ['name', 'gender', 'age'] - for field in required_fields: - if not data.get(field): - return jsonify({'success': False, 'error': f'{field}不能为空'}), 400 + # 验证患者数据 + validation_result = DataValidator.validate_patient_data(data) + if not validation_result['valid']: + return jsonify({ + 'success': False, + 'error': '; '.join(validation_result['errors']) + }), 400 + # 准备患者数据 patient_data = { - 'name': data['name'], - 'gender': data['gender'], - 'age': data['age'], - 'birth_date': data.get('birth_date'), - 'nationality': data.get('nationality'), - 'residence': data.get('residence'), - 'height': data.get('height'), - 'weight': data.get('weight'), - 'shoe_size': data.get('shoe_size'), - 'phone': data.get('phone'), - 'email': data.get('email'), - 'occupation': data.get('occupation'), - 'workplace': data.get('workplace'), - 'medical_history': data.get('medical_history', ''), - 'notes': data.get('notes', '') + 'name': validation_result['data'].get('name'), + 'gender': validation_result['data'].get('gender'), + 'birth_date': validation_result['data'].get('birth_date'), + 'nationality': validation_result['data'].get('nationality'), + 'residence': validation_result['data'].get('residence'), + 'height': validation_result['data'].get('height'), + 'weight': validation_result['data'].get('weight'), + 'shoe_size': validation_result['data'].get('shoe_size'), + 'phone': validation_result['data'].get('phone'), + 'email': validation_result['data'].get('email'), + 'occupation': validation_result['data'].get('occupation'), + 'workplace': validation_result['data'].get('workplace'), + 'idcode': validation_result['data'].get('idcode'), + 'medical_history': validation_result['data'].get('medical_history'), + 'notes': validation_result['data'].get('notes') } + # 创建患者 patient_id = self.db_manager.create_patient(patient_data) - if patient_id: - return jsonify({ - 'success': True, - 'message': '患者创建成功', - 'data': {'patient_id': patient_id} - }) - else: - return jsonify({'success': False, 'error': '患者创建失败'}), 500 - + # 获取创建的患者信息 + patient = self.db_manager.get_patient(patient_id) + + return jsonify({ + 'success': True, + 'message': '患者创建成功', + 'data': { + 'patient_id': patient_id, + 'patient': patient + } + }) + except Exception as e: self.logger.error(f'创建患者失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 @@ -1033,6 +1045,7 @@ class AppServer: 'email': data.get('email'), 'occupation': data.get('occupation'), 'workplace': data.get('workplace'), + 'idcode': data.get('idcode'), 'medical_history': data.get('medical_history'), 'notes': data.get('notes') } @@ -1076,21 +1089,8 @@ class AppServer: except Exception as e: self.logger.error(f'获取设备状态失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - - @self.app.route('/api/devices/refresh', methods=['POST']) - def refresh_devices(): - """刷新设备""" - try: - if self.device_coordinator: - result = self.device_coordinator.refresh_all_devices() - return jsonify({'success': True, 'data': result}) - else: - return jsonify({'success': False, 'error': '设备协调器未初始化'}), 500 - - except Exception as e: - self.logger.error(f'刷新设备失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 + return jsonify({'success': False, 'error': str(e)}), 500 + # ==================== 设备配置API ==================== @@ -1222,6 +1222,19 @@ class AppServer: except Exception as e: self.logger.error(f'开始检测失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 + + @self.app.route('/api/detection//has_data', methods=['GET']) + def has_session_detection_data(session_id: str): + """检查指定会话是否存在检测数据,用于判断单次检测是否有效""" + try: + if not self.db_manager: + return jsonify({'success': False, 'error': '数据库管理器未初始化'}), 500 + + exists = self.db_manager.has_session_detection_data(session_id) + return jsonify({'success': True, 'session_id': session_id, 'has_data': bool(exists)}) + except Exception as e: + self.logger.error(f'检查会话检测数据存在失败: {e}') + return jsonify({'success': False, 'error': str(e)}), 500 @self.app.route('/api/detection//stop', methods=['POST']) def stop_detection(session_id): @@ -1246,24 +1259,22 @@ class AppServer: 'message': '空白会话已删除' }) else: - # 正常会话的停止流程 - # 如果提供了duration,更新到数据库 + # 正常会话的停止流程:调用数据库层结束检测,自动计算时长并写入结束信息 data = flask_request.get_json() or {} - duration = data.get('duration') - if duration is not None and isinstance(duration, (int, float)): - try: - self.db_manager.update_session_duration(session_id, int(duration)) - self.logger.info(f'更新会话持续时间: {session_id} -> {duration}秒') - except Exception as duration_error: - self.logger.error(f'更新会话持续时间失败: {duration_error}') - - # 更新会话状态为已完成 - success = self.db_manager.update_session_status(session_id, 'completed') + diagnosis_info = data.get('diagnosis_info') + treatment_info = data.get('treatment_info') + suggestion_info = data.get('suggestion_info') + success = self.db_manager.update_session_endcheck( + session_id, + diagnosis_info=diagnosis_info, + treatment_info=treatment_info, + suggestion_info=suggestion_info + ) if success: - self.logger.info(f'检测会话已停止 - 会话ID: {session_id}') + self.logger.info(f'检测会话已结束检查 - 会话ID: {session_id}') return jsonify({ 'success': True, - 'message': '检测已停止' + 'message': '检测已结束并已写入总结信息' }) else: self.logger.error('停止检测失败,更新会话状态失败') @@ -1284,32 +1295,35 @@ class AppServer: data = flask_request.get_json() patient_id = data.get('patient_id') - screen_location = data.get('screen_location') # [0,0,1920,1080] - camera_location = data.get('camera_location') # [0,0,640,480] + screen_location = data.get('screen_location') # [0,0,1920,1080] femtobolt_location = data.get('femtobolt_location') # [0,0,640,480] - - + camera1_location = data.get('camera1_location') # [0,0,640,480] + camera2_location = data.get('camera2_location') # [0,0,640,480] if not patient_id: return jsonify({'success': False, 'error': '缺少患者ID'}), 400 # 开始视频录制 recording_response = None try: - recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera_location,femtobolt_location) + recording_response = self.recording_manager.start_recording(session_id, patient_id,screen_location,camera1_location,camera2_location,femtobolt_location) # 处理录制管理器返回的数据库更新信息 if recording_response and recording_response.get('success') and 'database_updates' in recording_response: db_updates = recording_response['database_updates'] try: - # 更新会话状态 - if not self.db_manager.update_session_status(db_updates['session_id'], db_updates['status']): - self.logger.error(f'更新会话状态失败 - 会话ID: {db_updates["session_id"]}, 状态: {db_updates["status"]}') - - # 更新视频文件路径 - video_paths = db_updates['video_paths'] - self.db_manager.update_session_normal_video_path(db_updates['session_id'], video_paths['normal_video_path']) - self.db_manager.update_session_screen_video_path(db_updates['session_id'], video_paths['screen_video_path']) - self.db_manager.update_session_femtobolt_video_path(db_updates['session_id'], video_paths['femtobolt_video_path']) + # 保存检测视频记录(映射到 detection_video 表字段) + video_paths = db_updates.get('video_paths', {}) + video_record = { + 'screen_video_path': video_paths.get('screen_video_path'), + 'femtobolt_video_path': video_paths.get('femtobolt_video_path'), + 'camera1_video_path': video_paths.get('camera1_video_path'), + 'camera2_video_path': video_paths.get('camera2_video_path'), + } + + try: + self.db_manager.save_detection_video(db_updates['session_id'], video_record) + except Exception as video_err: + self.logger.error(f'保存检测视频记录失败: {video_err}') self.logger.info(f'数据库更新成功 - 会话ID: {db_updates["session_id"]}') except Exception as db_error: @@ -1329,55 +1343,20 @@ class AppServer: def stop_record(session_id): """停止视频录制""" try: - if not self.db_manager or not self.device_coordinator: - self.logger.error('数据库管理器或设备管理器未初始化') - return jsonify({'success': False, 'error': '数据库管理器或设备管理器未初始化'}), 500 - if not session_id: self.logger.error('缺少会话ID') return jsonify({ 'success': False, 'error': '缺少会话ID' - }), 400 - + }), 400 # 停止同步录制,传递视频数据 try: restrt = self.recording_manager.stop_recording(session_id) - self.logger.info(f'停止录制结果: {restrt}') - - # 处理录制管理器返回的数据库更新信息 - if restrt and restrt.get('success') and 'database_updates' in restrt: - db_updates = restrt['database_updates'] - try: - # 更新会话状态 - success = self.db_manager.update_session_status(db_updates['session_id'], db_updates['status']) - self.logger.info(f'会话状态已更新为: {db_updates["status"]} - 会话ID: {db_updates["session_id"]}') - except Exception as db_error: - self.logger.error(f'处理停止录制的数据库更新失败: {db_error}') - success = False - else: - # 如果录制管理器没有返回数据库更新信息,则手动更新 - success = self.db_manager.update_session_status(session_id, 'recorded') - + self.logger.info(f'停止录制结果: {restrt}') except Exception as rec_e: - self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True) - # 即使录制停止失败,也尝试更新数据库状态 - success = self.db_manager.update_session_status(session_id, 'recorded') - raise - - if success: - self.logger.info(f'检测会话已停止 - 会话ID: {session_id}') - return jsonify({ - 'success': True, - 'message': '检测已停止' - }) - else: - self.logger.error('停止检测失败,更新会话状态失败') - return jsonify({ - 'success': False, - 'error': '停止检测失败' - }), 500 - + self.logger.error(f'停止同步录制失败: {rec_e}', exc_info=True) + raise + return jsonify({'success': True,'msg': '停止录制成功'}) except Exception as e: self.logger.error(f'停止检测失败: {e}', exc_info=True) return jsonify({'success': False, 'error': str(e)}), 500 @@ -1475,8 +1454,8 @@ class AppServer: self.logger.error(f'保存会话信息失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 - @self.app.route('/api/detection//collect', methods=['POST']) - def collect_detection_data(session_id): + @self.app.route('/api/detection//save-data', methods=['POST']) + def save_detection_data(session_id): """采集检测数据""" try: if not self.db_manager: @@ -1506,8 +1485,8 @@ class AppServer: 'error': '无法获取患者ID' }), 400 - # 调用录制管理器采集数据 - collected_data = self.recording_manager.collect_detection_data( + # 调用录制管理器保存检测截图到文件 + collected_data = self.recording_manager.save_detection_images( session_id=session_id, patient_id=patient_id, detection_data=data @@ -1542,16 +1521,7 @@ class AppServer: sessions = self.db_manager.get_detection_sessions(page, size, patient_id) total = self.db_manager.get_sessions_count(patient_id) - - # 为每个会话补充最新的检测数据 - for session in sessions: - session_id = session.get('id') - if session_id: - latest_data = self.db_manager.get_latest_detection_data(session_id, 5) - session['latest_detection_data'] = latest_data - else: - session['latest_detection_data'] = [] - + return jsonify({ 'success': True, 'data': { @@ -1583,57 +1553,74 @@ class AppServer: self.logger.error(f'获取会话数据失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 - - - @self.app.route('/api/detection/data//latest', methods=['GET']) - def get_latest_detection_data(session_id): - """获取最新的检测数据""" + + @self.app.route('/api/detection/data/details', methods=['GET']) + def get_detection_data_by_ids(): + """根据多个主键ID查询检测数据详情,ids为逗号分隔""" try: - limit = int(flask_request.args.get('limit', 5)) - data = self.db_manager.get_latest_detection_data(session_id, limit) - - return jsonify({ - 'success': True, - 'data': data - }) - + ids_param = flask_request.args.get('ids') + if not ids_param: + return jsonify({'success': False, 'error': '缺少ids参数'}), 400 + ids = [i.strip() for i in ids_param.split(',') if i.strip()] + data_list = self.db_manager.get_detection_data_by_ids(ids) + return jsonify({'success': True, 'data': data_list}) except Exception as e: - self.logger.error(f'获取最新检测数据失败: {e}') - return jsonify({'success': False, 'error': str(e)}), 500 - - @self.app.route('/api/detection/data/detail/', methods=['GET']) - def get_detection_data_by_id(data_id): - """根据主键ID查询检测数据详情""" - try: - data = self.db_manager.get_detection_data_by_id(data_id) - if data is None: - return jsonify({'success': False, 'error': '检测数据不存在'}), 404 - - return jsonify({ - 'success': True, - 'data': data - }) - - except Exception as e: - self.logger.error(f'获取检测数据详情失败: {e}') + self.logger.error(f'批量获取检测数据失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 @self.app.route('/api/detection/data/', methods=['DELETE']) def delete_detection_data(data_id): - """删除检测数据记录""" + """删除检测数据记录(支持单个或多个ID,多个用逗号分隔)""" try: - self.db_manager.delete_detection_data(data_id) - - return jsonify({ - 'success': True, - 'message': '检测数据删除成功' - }) + if not data_id: + return jsonify({'success': False, 'error': '未提供检测数据ID'}), 400 + + # 支持批量:逗号分隔 + ids = [i.strip() for i in str(data_id).split(',') if i.strip()] + payload = ids if len(ids) > 1 else (ids[0] if ids else data_id) + + success = self.db_manager.delete_detection_data(payload) + if success: + return jsonify({ + 'success': True, + 'message': '检测数据删除成功', + 'deleted_ids': ids + }) + else: + return jsonify({'success': False, 'error': '检测数据删除失败'}), 500 except ValueError as e: return jsonify({'success': False, 'error': str(e)}), 404 except Exception as e: self.logger.error(f'删除检测数据失败: {e}') return jsonify({'success': False, 'error': str(e)}), 500 + + @self.app.route('/api/detection/video/', methods=['DELETE']) + def delete_detection_video(video_id): + """删除检测视频记录(支持单个或多个ID,多个用逗号分隔)""" + try: + if not video_id: + return jsonify({'success': False, 'error': '未提供检测视频ID'}), 400 + + # 支持批量:逗号分隔 + ids = [i.strip() for i in str(video_id).split(',') if i.strip()] + payload = ids if len(ids) > 1 else (ids[0] if ids else video_id) + + success = self.db_manager.delete_detection_video(payload) + if success: + return jsonify({ + 'success': True, + 'message': '检测视频删除成功', + 'deleted_ids': ids + }) + else: + return jsonify({'success': False, 'error': '检测视频删除失败'}), 500 + + except ValueError as e: + return jsonify({'success': False, 'error': str(e)}), 404 + except Exception as e: + self.logger.error(f'删除检测视频失败: {e}') + return jsonify({'success': False, 'error': str(e)}), 500 @self.app.route('/api/detection/sessions/', methods=['DELETE']) def delete_detection_session(session_id): diff --git a/backend/utils.py b/backend/utils.py index 437dc9e9..57482ee1 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -222,8 +222,13 @@ class DataValidator: # 性别验证 if data.get('gender'): - if data['gender'] not in ['male', 'female', 'other']: - errors.append('性别值无效') + # 支持中文和英文性别值 + gender_map = {'男': 'male', '女': 'female', 'male': 'male', 'female': 'female'} + gender_value = data['gender'].strip() + if gender_value in gender_map: + data['gender'] = gender_map[gender_value] + else: + errors.append('性别值无效,应为:男、女、male、female') # 出生日期验证 if data.get('birth_date'): diff --git a/document/Web接口调用说明.md b/document/Web接口调用说明.md new file mode 100644 index 00000000..ffed68d4 --- /dev/null +++ b/document/Web接口调用说明.md @@ -0,0 +1,259 @@ +# Web 接口调用说明 + +本文档基于 `backend/main.py` 中注册的路由,整理对外提供的 Web API 调用方式、参数与返回示例,便于前端或第三方系统集成。 + +## 总览 + +- 基础与健康检查 + - `GET /health` + - `GET /api/health` +- 授权相关 + - `GET /api/license/info` + - `POST /api/license/activation-request` + - `POST /api/license/verify` + - `POST /api/license/activate-package` +- 认证与用户 + - `POST /api/auth/login` + - `POST /api/auth/register` + - `POST /api/auth/logout` + - `GET /api/auth/verify` + - `POST /api/auth/forgot-password` + - `GET /api/users` + - `POST /api/users//approve` + - `DELETE /api/users/` +- 设备与配置 + - `GET /api/devices/status` + - `POST /api/devices/refresh` + - `GET /api/config/devices` + - `POST /api/config/devices/all` + - `POST /api/devices/calibrate` + - `POST /api/devices/calibrate/imu` +- 患者管理 + - `GET /api/patients` + - `GET|PUT|DELETE /api/patients/` +- 检测流程 + - `POST /api/detection/start` + - `GET /api/detection//has_data` + - `POST /api/detection//start_record` + - `POST /api/detection//stop_record` + - `POST /api/detection//save-data` + - `POST /api/detection//save-info` + - `GET /api/detection//status` + - `POST /api/detection//stop` +- 历史与数据查询 + - `GET /api/history/sessions` + - `GET /api/history/sessions/` + - `GET /api/detection/data/details?ids=` +- 删除操作 + - `DELETE /api/detection/data/` + - `DELETE /api/detection/video/` + - `DELETE /api/detection/sessions/` + +## 统一响应约定 + +- 成功:`{ "success": true, ... }` +- 失败:`{ "success": false, "error": "错误信息" }` +- 时间字段统一使用 ISO 文本或 `YYYY-MM-DD HH:mm:ss` 字符串。 + +## 基础与授权 + +### GET /health | GET /api/health +- 功能:健康检查与服务存活状态。 +- 示例响应: +```json +{ "status": "healthy", "timestamp": "2024-01-01T12:00:00", "version": "1.0.0" } +``` + +### GET /api/license/info +- 功能:获取授权状态与基础信息。 +- 响应字段:`valid`, `message`, `license_type`, `license_id`, `expires_at`, `features`, `machine_id`。 + +### POST /api/license/activation-request +- 功能:生成离线激活请求文件。 +- Body:`{ "company_name": "公司名", "contact_info": "联系方式" }` +- 返回:`request_file` 路径与 `content` 文本。 + +### POST /api/license/verify +- 功能:上传并验证授权文件。 +- Form-Data:`license_file`(文件)。 +- 返回:授权验证结果与解析出的授权信息。 + +### POST /api/license/activate-package +- 功能:上传激活包进行离线激活。 +- 细节见服务端实现,返回激活状态与信息。 + +## 认证与用户 + +### POST /api/auth/login +- 功能:用户登录。 +- Body:`{ "username": "string", "password": "string" }` + +### POST /api/auth/register +- 功能:用户注册。 +- Body:包含用户名、密码、手机号等注册信息。 + +### POST /api/auth/logout +- 功能:登出当前会话。 + +### GET /api/auth/verify +- 功能:登录状态校验。 + +### POST /api/auth/forgot-password +- 功能:忘记密码,根据用户名和手机号找回。 +- Body:`{ "username": "string", "phone": "string" }` + +### GET /api/users +- 功能:获取用户列表。 +- Query 可选:分页参数。 + +### POST /api/users//approve +- 功能:批准用户,使其 `is_active = 1`。 +- Body 可选:`{ "approved_by": 123 }` + +### DELETE /api/users/ +- 功能:删除用户。 + +## 患者管理 + +### GET /api/patients +- 功能:获取患者列表。 +- Query 可选:分页筛选。 + +### POST /api/patients +- 功能:创建新患者。 +- Body:患者基本信息字段(姓名、性别、出生日期等)。 +- 必填字段:`name`(姓名)、`gender`(性别)、`birth_date`(出生日期) +- 可选字段:`phone`(电话)、`email`(邮箱)、`height`(身高)、`weight`(体重)、`nationality`(民族)、`residence`(居住地)、`occupation`(职业)、`workplace`(工作单位)、`idcode`(身份证号)、`medical_history`(病史)、`notes`(备注) + +### GET|PUT|DELETE /api/patients/ +- 功能:获取/更新/删除单个患者。 +- PUT Body:患者基本信息字段(姓名、性别、出生日期、联系方式等)。 + +## 设备与配置 + +### GET /api/devices/status +- 功能:获取各设备连接与工作状态。 + +### POST /api/devices/refresh +- 功能:刷新设备状态(重扫/重连)。 + +### GET /api/config/devices +- 功能:获取当前设备配置。 + +### POST /api/config/devices/all +- 功能:批量设置设备配置。 +- Body:设备配置对象集合。 + +### POST /api/devices/calibrate +- 功能:触发设备标定(通用)。 + +### POST /api/devices/calibrate/imu +- 功能:仅触发 IMU 设备标定。 + +## 检测流程 + +### POST /api/detection/start +- 功能:创建检测会话并启动设备连接监控。 +- Body:`{ "patient_id": "string", "creator_id": "string" }` +- 返回:`session_id`。 + +### GET /api/detection//has_data +- 功能:会话是否存在检测数据,用于判断单次检测是否有效。 +- 返回:`{ "has_data": true|false }` +### GET /api/detection//status +- 功能:获取会话最新状态与聚合数据(源自 `get_session_data`)。 + +### POST /api/detection//stop +- 功能:结束检测并写入总结信息(自动计算时长与结束时间)。 +- Body 可选: +```json +{ + "diagnosis_info": "string", + "treatment_info": "string", + "remark_info": "string" +} +``` +- 说明:服务端已统一走 `update_session_endcheck` 数据库流程。 +### POST /api/detection//start_record +- 功能:开始同步录制(屏幕/相机/设备流)。 +- Body 示例: +```json +{ + "patient_id": "p001", + "screen_location": [0,0,1920,1080], + "femtobolt_location": [0,0,640,480], + "camera1_location": [0,0,640,480], + "camera2_location": [0,0,640,480] +} +``` +- 返回:`recording` 含 `database_updates.video_paths`;服务端会自动调用 `save_detection_video` 记录视频路径(`screen_video_path`, `femtobolt_video_path`, `camera1_video_path`, `camera2_video_path`)。 + +### POST /api/detection//stop_record +- 功能:停止同步录制。 + +### POST /api/detection//save-data +- 功能:采集检测数据与截图并持久化。 +- Body:检测数据载荷(结构由实际采集模块决定)。 +- 返回:`timestamp` 与是否采集成功。 + +### POST /api/detection//save-info +- 功能:保存会话信息(诊断、处理、建议、状态)。 +- Body: +```json +{ + "diagnosis_info": "string", + "treatment_info": "string", + "remark_info": "string", + "status": "completed|running|..." +} +``` + + + +## 历史与数据查询 + +### GET /api/history/sessions +- 功能:分页获取检测会话历史。 +- Query:`page`(默认 1)、`size`(默认 10)、`patient_id`(可选)。 +- 返回:`sessions` 与 `total`。 + +### GET /api/history/sessions/ +- 功能:获取会话详细数据(聚合会话、检测数据、视频)。 + +### GET /api/detection/data/details?ids= +- 功能:根据多个逗号分隔的 ID 批量获取检测数据详情。 +- Query:`ids` 逗号分隔字符串。 + +## 删除操作 + +### DELETE /api/detection/data/ +- 功能:删除检测数据记录,支持多个 ID 逗号分隔。 +- 返回:`deleted_ids` 列表。 + +### DELETE /api/detection/video/ +- 功能:删除检测视频记录,支持多个 ID 逗号分隔。 +- 返回:`deleted_ids` 列表。 + +### DELETE /api/detection/sessions/ +- 功能:删除检测会话,同时清理关联的检测数据与视频记录。 + +## 错误码与常见失败 + +- 400:缺少必要参数或请求体格式错误。 +- 403:授权校验失败或未授权功能。 +- 404:会话或资源不存在。 +- 500:服务内部错误(设备不可用、数据库失败等)。 + +## 典型调用序列(示例) + +1. 创建会话:`POST /api/detection/start` +2. 开始录制:`POST /api/detection//start_record` +3. 采集数据:多次 `POST /api/detection//save-data` +4. 停止录制:`POST /api/detection//stop_record` +5. 保存诊断建议:`POST /api/detection//save-info` +6. 结束检测:`POST /api/detection//stop` +7. 校验有效性:`GET /api/detection//has_data` +8. 查询历史:`GET /api/history/sessions` +9. 查看详情:`GET /api/history/sessions/` + +> 注:具体字段与数据结构以采集模块与数据库模型为准,本文档以主干流程为纲要,建议结合实际返回进行前端适配与校验。 \ No newline at end of file diff --git a/frontend/src/renderer/src/views/Detection.vue b/frontend/src/renderer/src/views/Detection.vue index 94f25785..f087f07f 100644 --- a/frontend/src/renderer/src/views/Detection.vue +++ b/frontend/src/renderer/src/views/Detection.vue @@ -357,9 +357,21 @@ -
- +
+
+ camera1 +
+ 相机1 +
+
+
+ camera2 +
+ 相机2 +
+
@@ -567,7 +579,7 @@
-
@@ -591,12 +603,16 @@ const route = useRoute() const isRecording = ref(false) const isConnected = ref(false) const rtspImgSrc = ref('') +const camera1ImgSrc = ref('') +const camera2ImgSrc = ref('') const depthCameraImgSrc = ref('') // 深度相机视频流 const screenshotLoading = ref(false) const cameraDialogVisible =ref(false) // 设置相机参数弹框 const contenGridRef =ref(null) // 实时检查整体box const wholeBodyRef = ref(null) // 身体姿态ref const videoImgRef =ref(null) // 视频流图片ref +const camera1Ref = ref(null) +const camera2Ref = ref(null) const historyDialogVisible = ref(false) // 录像相关变量 let mediaRecorder = null @@ -1084,8 +1100,19 @@ function connectWebSocket() { // 监听各设备数据事件 devicesSocket.on('camera_frame', (data) => { frameCount++ - tempInfo.value.camera_frame = data - displayFrame(data.image) + // 区分 camera1 / camera2 帧 + const devId = (data && data.device_id) ? String(data.device_id).toLowerCase() : '' + if (!tempInfo.value.camera_frames) { + tempInfo.value.camera_frames = {} + } + if (devId === 'camera2') { + tempInfo.value.camera_frames['camera2'] = data + displayCameraFrameById('camera2', data.image) + } else { + // 默认 camera1(兼容旧逻辑 device_id 为空) + tempInfo.value.camera_frames['camera1'] = data + displayCameraFrameById('camera1', data.image) + } }) devicesSocket.on('femtobolt_frame', (data) => { @@ -1238,8 +1265,20 @@ function reconnectWebSocket() { // 简单的帧显示函数 function displayFrame(base64Image) { + // 兼容旧调用:默认作为 camera1 更新 + displayCameraFrameById('camera1', base64Image) +} + +function displayCameraFrameById(deviceId, base64Image) { if (base64Image && base64Image.length > 0) { - rtspImgSrc.value = 'data:image/jpeg;base64,' + base64Image + const url = 'data:image/jpeg;base64,' + base64Image + if (String(deviceId).toLowerCase() === 'camera2') { + camera2ImgSrc.value = url + } else { + camera1ImgSrc.value = url + // 旧变量保留(避免其它位置引用出错) + rtspImgSrc.value = url + } } else { console.warn('⚠️ 收到空的视频帧数据') } @@ -2054,7 +2093,8 @@ const startRecord = async () => { // 开始录屏 } let screen_location = contenGridRef.value.getBoundingClientRect() let femtobolt_location = wholeBodyRef.value.getBoundingClientRect() - let camera_location = videoImgRef.value.getBoundingClientRect() + let camera1_location = camera1Ref.value?.getBoundingClientRect() + let camera2_location = camera2Ref.value?.getBoundingClientRect() let titile_height = 24 // 调用后端API开始录屏 const response = await fetch(`${BACKEND_URL}/api/detection/${patientInfo.value.sessionId}/start_record`, { @@ -2067,7 +2107,14 @@ const startRecord = async () => { // 开始录屏 // 可以添加其他录屏参数 creator_id: creatorId.value, screen_location:[Math.round(screen_location.x), Math.round(screen_location.y) + titile_height, Math.round(screen_location.width), Math.round(screen_location.height)], - camera_location:[Math.round(camera_location.x), Math.round(camera_location.y)+ titile_height, Math.round(camera_location.width), Math.round(camera_location.height)], + camera1_location:[ + Math.round(camera1_location.x), Math.round(camera1_location.y)+ titile_height, + Math.round(camera1_location.width), Math.round(camera1_location.height) + ], + camera2_location:[ + Math.round(camera2_location.x), Math.round(camera2_location.y)+ titile_height, + Math.round(camera2_location.width), Math.round(camera2_location.height) + ], femtobolt_location:[Math.round(femtobolt_location.x), Math.round(femtobolt_location.y) + titile_height, Math.round(femtobolt_location.width), Math.round(femtobolt_location.height)], }) diff --git a/frontend/src/renderer/src/views/PatientCreate.vue b/frontend/src/renderer/src/views/PatientCreate.vue index 06ef428d..3f8738ca 100644 --- a/frontend/src/renderer/src/views/PatientCreate.vue +++ b/frontend/src/renderer/src/views/PatientCreate.vue @@ -258,21 +258,25 @@ const validateForm = async () => { } const savePatient = async () => { + // 性别值映射:中文转英文 + const genderMap = { '男': 'male', '女': 'female' } + const genderValue = genderMap[patientForm.gender] || patientForm.gender + const patientData = { - id: patientForm.id, name: patientForm.name, - gender: patientForm.gender, - age: calculatedAge.value, + gender: genderValue, birth_date: patientForm.birth_date, - height: patientForm.height, - weight: patientForm.weight, - shoe_size: patientForm.shoe_size, + height: parseFloat(patientForm.height) || null, + weight: parseFloat(patientForm.weight) || null, + shoe_size: patientForm.shoe_size ? parseFloat(patientForm.shoe_size) : null, phone: patientForm.phone, occupation: patientForm.occupation, email: patientForm.email, nationality: patientForm.nationality, residence: patientForm.residence, - workplace: patientForm.workplace + workplace: patientForm.workplace, + medical_history: '', // 添加病史字段 + notes: '' // 添加备注字段 } try {