""" 服务层模块 提供系统核心业务逻辑服务,包括: - 安全认证服务(双因子认证、会话管理) - 传感器型号配置管理服务 - 测试执行引擎服务 - 数据采集与监控服务 - 特征参数计算服务 - 报告生成服务 - 异常处理与自恢复服务 """ import os import time import math import struct import hashlib import threading from datetime import datetime from typing import Optional, Callable from app.data_structures import ( UserInfo, UserRole, SensorModelConfig, TestContext, AcquisitionDataPoint, JudgmentResult, TestResult, ReportContent, SystemStatus, SystemMode, RingBuffer ) # ============================================================================= # 安全认证服务 # ============================================================================= class SecurityService: """ 安全认证服务 提供双因子认证(Windows域账号 + TOTP动态口令)、会话管理和权限控制。 遵循 GJB 5000B 安全要求。 """ # 密码错误上限 MAX_FAILED_ATTEMPTS = 5 def __init__(self): """初始化安全服务,创建默认用户""" self._users: dict[str, UserInfo] = {} self._current_session: Optional[UserInfo] = None self._init_default_users() def _init_default_users(self) -> None: """初始化默认用户(仅用于演示,生产环境通过域服务管理)""" self._users["admin"] = UserInfo( user_id="admin", role=UserRole.ADMIN, domain_account="SENSOR\\admin", failed_attempts=0, is_locked=False ) self._users["tech01"] = UserInfo( user_id="tech01", role=UserRole.TECHNICIAN, domain_account="SENSOR\\tech01", failed_attempts=0, is_locked=False ) self._users["op01"] = UserInfo( user_id="op01", role=UserRole.OPERATOR, domain_account="SENSOR\\op01", failed_attempts=0, is_locked=False ) def verify_static_password(self, username: str, password: str) -> bool: """ 验证静态密码(模拟Windows域认证) Args: username: 用户名 password: 密码 Returns: 验证通过返回True """ # 模拟域认证:所有用户密码为 "pass123" return password == "pass123" def verify_totp(self, token: str) -> bool: """ 验证TOTP动态口令 Args: token: 动态口令(6位数字) Returns: 验证通过返回True Note: 遵循 RFC 6238 标准。此处使用模拟验证。 """ # 模拟TOTP验证:口令为 "123456" 时通过 # 正式环境使用 pyotp 库实现 RFC 6238 return token == "123456" def login(self, username: str, password: str, totp_token: str) -> tuple[bool, str]: """ 用户登录(双因子认证) 流程: 1. 检查用户是否存在且未被锁定 2. 验证TOTP动态口令 3. 验证静态密码 4. 创建会话令牌 Args: username: 用户名 password: 静态密码 totp_token: 动态口令 Returns: (是否成功, 消息) """ user = self._users.get(username) if not user: return False, "用户不存在" if user.is_locked: return False, f"账户已锁定,请联系管理员解锁" # 第一步:验证TOTP if not self.verify_totp(totp_token): user.failed_attempts += 1 if user.failed_attempts >= self.MAX_FAILED_ATTEMPTS: user.is_locked = True return False, f"密码错误超限,账户已锁定" remaining = self.MAX_FAILED_ATTEMPTS - user.failed_attempts return False, f"动态口令错误,剩余尝试次数:{remaining}" # 第二步:验证静态密码 if not self.verify_static_password(username, password): user.failed_attempts += 1 if user.failed_attempts >= self.MAX_FAILED_ATTEMPTS: user.is_locked = True return False, f"密码错误超限,账户已锁定" remaining = self.MAX_FAILED_ATTEMPTS - user.failed_attempts return False, f"密码错误,剩余尝试次数:{remaining}" # 登录成功 user.failed_attempts = 0 user.session_token = hashlib.sha256( f"{username}{time.time()}".encode() ).hexdigest() self._current_session = user return True, "登录成功" def logout(self) -> None: """注销当前会话""" if self._current_session: self._current_session.session_token = None self._current_session = None def unlock_user(self, admin_user: str, target_user: str) -> bool: """ 管理员解锁用户 Args: admin_user: 管理员用户名 target_user: 目标用户名 Returns: 解锁成功返回True """ admin = self._users.get(admin_user) if not admin or admin.role != UserRole.ADMIN: return False user = self._users.get(target_user) if not user: return False user.is_locked = False user.failed_attempts = 0 return True def get_current_user(self) -> Optional[UserInfo]: """获取当前登录用户""" return self._current_session def has_permission(self, required_role: UserRole) -> bool: """ 检查当前用户是否拥有指定权限 Args: required_role: 所需最低角色 Returns: 有权限返回True """ if not self._current_session: return False role_rank = { UserRole.OPERATOR: 1, UserRole.TECHNICIAN: 2, UserRole.ADMIN: 3 } return role_rank.get(self._current_session.role, 0) >= role_rank.get(required_role, 0) def check_usb_certificate(self, cert: bytes) -> bool: """ 检查USB设备数字证书 Args: cert: 证书数据 Returns: 认证通过返回True """ # 模拟证书检查:非空且长度大于0则通过 return len(cert) > 0 def wipe_memory_region(self, addr, size) -> None: """ 擦除内存区域(安全清零) Args: addr: 内存地址(模拟) size: 区域大小 """ # 模拟内存擦除操作 pass # ============================================================================= # 传感器型号配置管理服务 # ============================================================================= class SensorConfigService: """ 传感器型号配置管理服务 工艺员可在内存中增删改传感器型号参数。 所有数据仅驻留内存,退出时自动覆写清零。 """ def __init__(self): """初始化配置服务,创建默认型号配置""" self._configs: dict[str, SensorModelConfig] = {} self._init_default_configs() def _init_default_configs(self) -> None: """初始化默认传感器型号配置""" defaults = [ SensorModelConfig("SENSOR-001", 0.0, 10.0, 5, 3, 0.5), SensorModelConfig("SENSOR-002", 0.0, 25.0, 6, 3, 0.3), SensorModelConfig("SENSOR-003", 0.0, 60.0, 8, 2, 0.2), SensorModelConfig("SENSOR-004", -0.1, 1.0, 4, 3, 1.0), ] for cfg in defaults: self._configs[cfg.model_id] = cfg def get_config(self, model_id: str) -> Optional[SensorModelConfig]: """ 获取型号配置 Args: model_id: 型号ID Returns: 配置对象,不存在时返回None """ return self._configs.get(model_id) def get_all_configs(self) -> dict[str, SensorModelConfig]: """获取所有型号配置""" return dict(self._configs) def add_config(self, config: SensorModelConfig) -> bool: """ 添加型号配置 Args: config: 配置对象 Returns: 添加成功返回True """ if config.model_id in self._configs: return False self._configs[config.model_id] = config return True def update_config(self, config: SensorModelConfig) -> bool: """ 更新型号配置 Args: config: 配置对象 Returns: 更新成功返回True """ if config.model_id not in self._configs: return False self._configs[config.model_id] = config return True def delete_config(self, model_id: str) -> bool: """ 删除型号配置 Args: model_id: 型号ID Returns: 删除成功返回True """ return self._configs.pop(model_id, None) is not None def generate_pressure_sequence(self, model_id: str) -> list[float]: """ 根据型号配置生成压力序列(正反行程) 正行程:从 range_min 到 range_max,均匀分布 反行程:从 range_max 到 range_min,均匀分布 Args: model_id: 型号ID Returns: 压力序列列表 """ config = self.get_config(model_id) if not config: return [] points = config.test_points min_p = config.range_min max_p = config.range_max step = (max_p - min_p) / (points - 1) if points > 1 else 0 forward = [min_p + i * step for i in range(points)] backward = [max_p - i * step for i in range(points)] return forward + backward[1:] # 去掉重复的端点 def clear_all(self) -> None: """清空所有配置(安全覆写)""" for key in list(self._configs.keys()): cfg = self._configs[key] # 覆写敏感字段 cfg.range_min = 0.0 cfg.range_max = 0.0 cfg.tolerance = 0.0 del self._configs[key] # ============================================================================= # 测试执行引擎服务 # ============================================================================= class TestEngine: """ 测试执行引擎 负责管理自动化测试序列的执行,包括: - 压力控制器通信(模拟) - DAQ数据采集触发(模拟) - 暂停/继续/跳步控制 - 异常检测与恢复 """ def __init__(self, sensor_config_service: SensorConfigService): """ 初始化测试引擎 Args: sensor_config_service: 传感器配置服务实例 """ self._config_service = sensor_config_service self._context = TestContext() self._system_status = SystemStatus() self._on_data_point: Optional[Callable] = None self._on_status_change: Optional[Callable] = None self._reconnect_timer: Optional[threading.Timer] = None self._lock = threading.Lock() @property def context(self) -> TestContext: """获取测试上下文""" return self._context @property def system_status(self) -> SystemStatus: """获取系统状态""" return self._system_status def set_data_callback(self, callback: Callable) -> None: """ 设置数据采集回调 Args: callback: 回调函数,接收 AcquisitionDataPoint 参数 """ self._on_data_point = callback def set_status_callback(self, callback: Callable) -> None: """ 设置状态变更回调 Args: callback: 回调函数 """ self._on_status_change = callback def load_test_plan(self, model_id: str) -> bool: """ 加载测试计划 根据型号ID从配置服务获取参数模板,生成压力序列。 Args: model_id: 传感器型号ID Returns: 加载成功返回True """ config = self._config_service.get_config(model_id) if not config: return False with self._lock: self._context.reset() self._context.current_model = model_id self._context.pressure_sequence = ( self._config_service.generate_pressure_sequence(model_id) ) self._context.current_cycle = 0 self._context.current_point_index = 0 return True def start_test(self) -> bool: """ 启动测试 Returns: 启动成功返回True """ with self._lock: if not self._context.pressure_sequence: return False self._context.status_flags["is_running"] = True self._context.status_flags["is_paused"] = False self._context.status_flags["alarm_active"] = False self._context.current_cycle = 1 self._context.current_point_index = 0 self._notify_status_change() return True def pause_test(self) -> bool: """ 暂停测试 Returns: 暂停成功返回True """ with self._lock: if not self._context.status_flags["is_running"]: return False self._context.status_flags["is_paused"] = True self._notify_status_change() return True def resume_test(self) -> bool: """ 继续测试 Returns: 继续成功返回True """ with self._lock: if not self._context.status_flags["is_paused"]: return False self._context.status_flags["is_paused"] = False self._notify_status_change() return True def skip_to_point(self, point_index: int) -> bool: """ 跳转到指定测试点 Args: point_index: 目标测试点索引 Returns: 跳转成功返回True """ with self._lock: total = len(self._context.pressure_sequence) if point_index < 0 or point_index >= total: return False self._context.current_point_index = point_index self._notify_status_change() return True def stop_test(self) -> None: """停止测试""" with self._lock: self._context.status_flags["is_running"] = False self._context.status_flags["is_paused"] = False self._notify_status_change() def simulate_acquisition(self) -> AcquisitionDataPoint: """ 模拟一次数据采集 模拟向压力控制器发送命令并读取DAQ数据。 在实际系统中,此方法将通过硬件接口与物理设备通信。 Returns: 采集数据点 """ with self._lock: if not self._context.status_flags["is_running"]: return AcquisitionDataPoint(0.0, 0.0, time.time()) idx = self._context.current_point_index sequence = self._context.pressure_sequence if idx >= len(sequence): return AcquisitionDataPoint(0.0, 0.0, time.time()) target_pressure = sequence[idx] # 模拟传感器输出 = 压力值 * (1 + 小随机误差) import random noise = random.uniform(-0.01, 0.01) output = target_pressure * (1 + noise) point = AcquisitionDataPoint( pressure=target_pressure, output=output, timestamp=time.time() ) # 存入缓冲区 self._context.acquisition_buffer.append(point) # 更新索引 self._context.current_point_index += 1 if self._context.current_point_index >= len(sequence): # 完成一个循环 total_cycles = self._get_total_cycles() if self._context.current_cycle < total_cycles: self._context.current_cycle += 1 self._context.current_point_index = 0 else: self._context.status_flags["is_running"] = False # 检测异常 if abs(point.pressure - target_pressure) > 0.5: self._context.status_flags["alarm_active"] = True else: self._context.status_flags["alarm_active"] = False if self._on_data_point: self._on_data_point(point) self._notify_status_change() return point def _get_total_cycles(self) -> int: """获取总循环次数""" config = self._config_service.get_config(self._context.current_model) return config.cycles if config else 3 def detect_communication_loss(self) -> bool: """ 检测通讯是否中断(模拟) Returns: True表示通讯中断 """ # 模拟:随机检测通讯状态,99%概率正常 import random return random.random() < 0.01 def attempt_reconnect(self) -> bool: """ 尝试重新连接设备 Returns: 重连成功返回True """ with self._lock: self._system_status.system_mode = SystemMode.NA self._system_status.instrument_status["pressure_ctrl"] = False self._system_status.instrument_status["daq"] = False # 模拟重连过程 time.sleep(0.5) with self._lock: self._system_status.system_mode = SystemMode.NORMAL self._system_status.instrument_status["pressure_ctrl"] = True self._system_status.instrument_status["daq"] = True self._notify_status_change() return True def start_reconnect_loop(self) -> None: """启动重连循环(每10秒尝试一次)""" def _reconnect_loop(): if self._system_status.system_mode == SystemMode.NA: if self.attempt_reconnect(): self._system_status.system_mode = SystemMode.NORMAL else: self._reconnect_timer = threading.Timer(10.0, _reconnect_loop) self._reconnect_timer.daemon = True self._reconnect_timer.start() self._reconnect_timer = threading.Timer(10.0, _reconnect_loop) self._reconnect_timer.daemon = True self._reconnect_timer.start() def _notify_status_change(self) -> None: """通知状态变更""" if self._on_status_change: self._on_status_change() def shutdown(self) -> None: """关闭引擎,清理资源""" self.stop_test() if self._reconnect_timer: self._reconnect_timer.cancel() self._context.acquisition_buffer.clear() # ============================================================================= # 特征参数计算服务 # ============================================================================= class CalculationService: """ 特征参数计算服务 基于最小二乘法计算非线性、迟滞、重复性误差。 计算耗时目标 ≤ 0.5秒。 """ @staticmethod def _least_squares_fit( x: list[float], y: list[float] ) -> tuple[float, float]: """ 最小二乘法线性拟合 y = a * x + b Args: x: 自变量列表 y: 因变量列表 Returns: (a, b) 拟合系数 """ n = len(x) if n == 0: return 0.0, 0.0 sum_x = sum(x) sum_y = sum(y) sum_xy = sum(xi * yi for xi, yi in zip(x, y)) sum_xx = sum(xi * xi for xi in x) denominator = n * sum_xx - sum_x * sum_x if abs(denominator) < 1e-12: return 0.0, sum_y / n a = (n * sum_xy - sum_x * sum_y) / denominator b = (sum_y * sum_xx - sum_x * sum_xy) / denominator return a, b def calculate( self, data_points: list[AcquisitionDataPoint], config: SensorModelConfig ) -> JudgmentResult: """ 计算测试结果判定 基于采集数据点计算非线性、迟滞、重复性误差, 并与配置阈值比较,输出PASS/FAIL结果。 Args: data_points: 采集数据点列表 config: 传感器型号配置 Returns: 判定结果对象 """ if not data_points: return JudgmentResult( non_linearity=0.0, hysteresis=0.0, repeatability=0.0, thresholds={"tolerance": config.tolerance}, result=TestResult.FAIL, details="无采集数据" ) # 提取压力值和输出值 pressures = [p.pressure for p in data_points] outputs = [p.output for p in data_points] # 最小二乘线性拟合(理论直线) a, b = self._least_squares_fit(pressures, outputs) # 计算非线性误差(最大偏差 / 满量程) full_scale = config.range_max - config.range_min if full_scale == 0: full_scale = 1.0 deviations = [ abs(out - (a * press + b)) / full_scale * 100 for press, out in zip(pressures, outputs) ] non_linearity = max(deviations) if deviations else 0.0 # 计算迟滞误差(正反行程差异) # 简化计算:取正行程和反行程的均值差异 half = len(pressures) // 2 if half > 1: forward_outputs = outputs[:half] backward_outputs = outputs[half:2 * half] if forward_outputs and backward_outputs: hysteresis = abs( sum(forward_outputs) / len(forward_outputs) - sum(backward_outputs) / len(backward_outputs) ) / full_scale * 100 else: hysteresis = 0.0 else: hysteresis = 0.0 # 计算重复性误差(简化:取标准偏差的百分比) if len(outputs) > 1: mean = sum(outputs) / len(outputs) variance = sum((o - mean) ** 2 for o in outputs) / len(outputs) std_dev = math.sqrt(variance) repeatability = (std_dev / full_scale) * 100 if full_scale > 0 else 0.0 else: repeatability = 0.0 # 判定 max_error = max(non_linearity, hysteresis, repeatability) passed = max_error <= config.tolerance details = "" if not passed: issues = [] if non_linearity > config.tolerance: issues.append(f"非线性误差 {non_linearity:.4f}% 超限(允差 {config.tolerance}%)") if hysteresis > config.tolerance: issues.append(f"迟滞误差 {hysteresis:.4f}% 超限(允差 {config.tolerance}%)") if repeatability > config.tolerance: issues.append(f"重复性误差 {repeatability:.4f}% 超限(允差 {config.tolerance}%)") details = "; ".join(issues) return JudgmentResult( non_linearity=round(non_linearity, 4), hysteresis=round(hysteresis, 4), repeatability=round(repeatability, 4), thresholds={"tolerance": config.tolerance}, result=TestResult.PASS if passed else TestResult.FAIL, details=details ) # ============================================================================= # 报告生成服务 # ============================================================================= class ReportService: """ 报告生成服务 生成PDF/A格式归档报告和CSV原始数据文件。 嵌入SM3哈希值与数字水印,确保完整性与防篡改。 生成时间目标 ≤ 3秒。 """ def __init__(self): """初始化报告服务""" self._test_counter = 0 def generate_report( self, context: TestContext, judgment: JudgmentResult, operator_id: str ) -> ReportContent: """ 生成测试报告 Args: context: 测试上下文 judgment: 判定结果 operator_id: 操作员ID Returns: 报告内容对象 """ self._test_counter += 1 now = datetime.now() report = ReportContent( test_id=f"TEST-{now.strftime('%Y%m%d')}-{self._test_counter:04d}", start_time=now, end_time=now, operator_id=operator_id, model_id=context.current_model, result_summary={ "non_linearity": judgment.non_linearity, "hysteresis": judgment.hysteresis, "repeatability": judgment.repeatability, "result": judgment.result.value, "details": judgment.details }, raw_data_table=[ [p.pressure, p.output, p.timestamp] for p in context.acquisition_buffer.get_all() ] ) # 生成数字水印(模拟) report.digital_watermark = ( f"SENSOR-TEST-{report.test_id}-" f"{now.strftime('%Y%m%d%H%M%S')}-" f"{operator_id}" ) # 计算SM3哈希(使用gmssl库模拟) report.sm3_hash = self._calculate_sm3_hash(report) return report def _calculate_sm3_hash(self, report: ReportContent) -> str: """ 计算SM3哈希值 对报告关键内容进行SM3哈希,确保完整性。 Args: report: 报告内容 Returns: SM3哈希值(十六进制字符串) """ # 使用标准hashlib模拟SM3(正式环境使用gmssl库的SM3算法) content = ( f"{report.test_id}{report.operator_id}{report.model_id}" f"{report.result_summary}{report.digital_watermark}" ).encode('utf-8') return hashlib.sha256(content).hexdigest() def generate_pdf_report(self, report: ReportContent) -> bytes: """ 生成PDF/A格式报告 使用reportlab库生成PDF文档。 实际系统中会生成完整的PDF/A格式报告,包含: - 测试结论 - 数据表 - 曲线图 - 操作员签名 - SM3哈希元数据 - 数字水印 Args: report: 报告内容 Returns: PDF文件字节数据 """ try: from io import BytesIO from reportlab.lib.pagesizes import A4 from reportlab.lib import colors from reportlab.lib.styles import getSampleStyleSheet from reportlab.platypus import ( SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle ) buffer = BytesIO() doc = SimpleDocTemplate( buffer, pagesize=A4, title=f"传感器测试报告 - {report.test_id}", author=report.operator_id ) styles = getSampleStyleSheet() elements = [] # 标题 elements.append(Paragraph( f"传感器测试报告", styles['Title'] )) elements.append(Spacer(1, 12)) # 基本信息 info_data = [ ["测试编号", report.test_id], ["开始时间", str(report.start_time)], ["结束时间", str(report.end_time)], ["操作员", report.operator_id], ["传感器型号", report.model_id], ["测试结果", report.result_summary.get("result", "N/A")], ] info_table = Table(info_data, colWidths=[120, 300]) info_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (0, -1), colors.lightgrey), ('GRID', (0, 0), (-1, -1), 0.5, colors.grey), ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 0), (-1, -1), 10), ])) elements.append(info_table) elements.append(Spacer(1, 20)) # 判定结果 elements.append(Paragraph( f"特征参数计算与判定", styles['Heading2'] )) elements.append(Spacer(1, 8)) calc_data = [ ["参数", "计算值", "允差", "状态"], [ "非线性误差", f"{report.result_summary.get('non_linearity', 'N/A')}%", f"{report.result_summary.get('non_linearity', 0)}%", "PASS" if report.result_summary.get('non_linearity', 0) <= 0.5 else "FAIL" ], [ "迟滞误差", f"{report.result_summary.get('hysteresis', 'N/A')}%", f"{report.result_summary.get('hysteresis', 0)}%", "PASS" if report.result_summary.get('hysteresis', 0) <= 0.5 else "FAIL" ], [ "重复性误差", f"{report.result_summary.get('repeatability', 'N/A')}%", f"{report.result_summary.get('repeatability', 0)}%", "PASS" if report.result_summary.get('repeatability', 0) <= 0.5 else "FAIL" ], ] calc_table = Table(calc_data, colWidths=[120, 100, 100, 80]) calc_table.setStyle(TableStyle([ ('BACKGROUND', (0, 0), (-1, 0), colors.lightgrey), ('GRID', (0, 0), (-1, -1), 0.5, colors.grey), ('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 0), (-1, -1), 10), ])) elements.append(calc_table) elements.append(Spacer(1, 12)) # 详细说明 if report.result_summary.get("details"): elements.append(Paragraph( f"异常说明:{report.result_summary['details']}", styles['Normal'] )) elements.append(Spacer(1, 12)) # SM3哈希 elements.append(Paragraph( f"SM3哈希值:{report.sm3_hash}", styles['Normal'] )) elements.append(Spacer(1, 6)) # 数字水印 elements.append(Paragraph( f"数字水印:{report.digital_watermark}", styles['Normal'] )) doc.build(elements) pdf_data = buffer.getvalue() buffer.close() return pdf_data except ImportError: # reportlab未安装时返回模拟数据 return f"PDF Report: {report.test_id} (simulated)".encode('utf-8') def generate_csv_data(self, report: ReportContent) -> bytes: """ 生成CSV原始数据文件 Args: report: 报告内容 Returns: CSV文件字节数据 """ lines = ["压力(MPa),输出值,时间戳"] for row in report.raw_data_table: lines.append(f"{row[0]},{row[1]},{row[2]}") return "\n".join(lines).encode('utf-8')