CGQ_TEST_DEVICE/app/services.py

1006 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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