"""
服务层模块
提供系统核心业务逻辑服务,包括:
- 安全认证服务(双因子认证、会话管理)
- 传感器型号配置管理服务
- 测试执行引擎服务
- 数据采集与监控服务
- 特征参数计算服务
- 报告生成服务
- 异常处理与自恢复服务
"""
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')