1006 lines
31 KiB
Python
1006 lines
31 KiB
Python
"""
|
||
服务层模块
|
||
|
||
提供系统核心业务逻辑服务,包括:
|
||
- 安全认证服务(双因子认证、会话管理)
|
||
- 传感器型号配置管理服务
|
||
- 测试执行引擎服务
|
||
- 数据采集与监控服务
|
||
- 特征参数计算服务
|
||
- 报告生成服务
|
||
- 异常处理与自恢复服务
|
||
"""
|
||
|
||
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"<b>特征参数计算与判定</b>", 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"<b>异常说明:</b>{report.result_summary['details']}",
|
||
styles['Normal']
|
||
))
|
||
elements.append(Spacer(1, 12))
|
||
|
||
# SM3哈希
|
||
elements.append(Paragraph(
|
||
f"<b>SM3哈希值:</b>{report.sm3_hash}", styles['Normal']
|
||
))
|
||
elements.append(Spacer(1, 6))
|
||
|
||
# 数字水印
|
||
elements.append(Paragraph(
|
||
f"<b>数字水印:</b>{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')
|