mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2025-12-15 13:44:39 +00:00
* 更新Bioyond工作站配置,添加新的物料类型映射和载架定义,优化物料查询逻辑 * 添加Bioyond实验配置文件,定义物料类型映射和设备配置 * 更新bioyond_warehouse_reagent_stack方法,修正试剂堆栈尺寸和布局描述 * 更新Bioyond实验配置,修正物料类型映射,优化设备配置 * 更新Bioyond资源同步逻辑,优化物料入库流程,增强错误处理和日志记录 * 更新Bioyond资源,添加配液站和反应站专用载架,优化仓库工厂函数的排序方式 * 更新Bioyond资源,添加配液站和反应站相关载架,优化试剂瓶和样品瓶配置 * 更新Bioyond实验配置,修正试剂瓶载架ID,确保与设备匹配 * 更新Bioyond资源,移除反应站单烧杯载架,添加反应站单烧瓶载架分类 * Refactor Bioyond resource synchronization and update bottle carrier definitions - Removed traceback printing in error handling for Bioyond synchronization. - Enhanced logging for existing Bioyond material ID usage during synchronization. - Added new bottle carrier definitions for single flask and updated existing ones. - Refactored dispensing station and reaction station bottle definitions for clarity and consistency. - Improved resource mapping and error handling in graphio for Bioyond resource conversion. - Introduced layout parameter in warehouse factory for better warehouse configuration. * 更新Bioyond仓库工厂,添加排序方式支持,优化坐标计算逻辑 * 更新Bioyond载架和甲板配置,调整样品板尺寸和仓库坐标 * 更新Bioyond资源同步,增强占用位置日志信息,修正坐标转换逻辑 * 更新Bioyond反应站和分配站配置,调整材料类型映射和ID,移除不必要的项 * support name change during materials change * fix json dumps * correct tip * 优化调度器API路径,更新相关方法描述 * 更新 BIOYOND 载架相关文档,调整 API 以支持自带试剂瓶的载架类型,修复资源获取时的子物料处理逻辑 * 实现资源删除时的同步处理,优化出库操作逻辑 * 修复 ItemizedCarrier 中的可见性逻辑 * 保存 Bioyond 原始信息到 unilabos_extra,以便出库时查询 * 根据 resource.capacity 判断是试剂瓶(载架)还是多瓶载架,走不同的奔曜转换 * Fix bioyond bottle_carriers ordering * 优化 Bioyond 物料同步逻辑,增强坐标解析和位置更新处理 * disable slave connect websocket * correct remove_resource stats * change uuid logger to trace level * enable slave mode * refactor(bioyond): 统一资源命名并优化物料同步逻辑 - 将DispensingStation和ReactionStation资源统一为PolymerStation命名 - 优化物料同步逻辑,支持耗材类型(typeMode=0)的查询 - 添加物料默认参数配置功能 - 调整仓库坐标布局 - 清理废弃资源定义 * feat(warehouses): 为仓库函数添加col_offset和layout参数 * refactor: 更新实验配置中的物料类型映射命名 将DispensingStation和ReactionStation的物料类型映射统一更名为PolymerStation,保持命名一致性 * fix: 更新实验配置中的载体名称从6VialCarrier到6StockCarrier * feat(bioyond): 实现物料创建与入库分离逻辑 将物料同步流程拆分为两个独立阶段:transfer阶段只创建物料,add阶段执行入库 简化状态检查接口,仅返回连接状态 * fix(reaction_station): 修正液体进料烧杯体积单位并增强返回结果 将液体进料烧杯的体积单位从μL改为g以匹配实际使用场景 在返回结果中添加merged_workflow和order_params字段,提供更完整的工作流信息 * feat(dispensing_station): 在任务创建返回结果中添加order_params信息 在create_order方法返回结果中增加order_params字段,以便调用方获取完整的任务参数 * fix(dispensing_station): 修改90%物料分配逻辑从分成3份改为直接使用 原逻辑将主称固体平均分成3份作为90%物料,现改为直接使用main_portion * feat(bioyond): 添加任务编码和任务ID的输出,支持批量任务创建后的状态监控 * refactor(registry): 简化设备配置中的任务结果处理逻辑 将多个单独的任务编码和ID字段合并为统一的return_info字段 更新相关描述以反映新的数据结构 * feat(工作站): 添加HTTP报送服务和任务完成状态跟踪 - 在graphio.py中添加API必需字段 - 实现工作站HTTP服务启动和停止逻辑 - 添加任务完成状态跟踪字典和等待方法 - 重写任务完成报送处理方法记录状态 - 支持批量任务完成等待和报告获取 * refactor(dispensing_station): 移除wait_for_order_completion_and_get_report功能 该功能已被wait_for_multiple_orders_and_get_reports替代,简化代码结构 * fix: 更新任务报告API错误 * fix(workstation_http_service): 修复状态查询中device_id获取逻辑 处理状态查询时安全获取device_id,避免因属性不存在导致的异常 * fix(bioyond_studio): 改进物料入库失败时的错误处理和日志记录 在物料入库API调用失败时,添加更详细的错误信息打印 同时修正station.py中对空响应和失败情况的判断逻辑 * refactor(bioyond): 优化瓶架载体的分配逻辑和注释说明 重构瓶架载体的分配逻辑,使用嵌套循环替代硬编码索引分配 添加更详细的坐标映射说明,明确PLR与Bioyond坐标的对应关系 * fix(bioyond_rpc): 修复物料入库成功时无data字段返回空的问题 当API返回成功但无data字段时,返回包含success标识的字典而非空字典 --------- Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Co-authored-by: Junhan Chang <changjh@dp.tech>
727 lines
29 KiB
Python
727 lines
29 KiB
Python
"""
|
||
工作站HTTP服务模块
|
||
Workstation HTTP Service Module
|
||
|
||
统一的工作站报送接收服务,基于LIMS协议规范:
|
||
1. 步骤完成报送 - POST /report/step_finish
|
||
2. 通量完成报送 - POST /report/sample_finish
|
||
3. 任务完成报送 - POST /report/order_finish
|
||
4. 批量更新报送 - POST /report/batch_update
|
||
5. 物料变更报送 - POST /report/material_change
|
||
6. 错误处理报送 - POST /report/error_handling
|
||
7. 健康检查和状态查询
|
||
|
||
统一使用LIMS协议字段规范,简化接口避免功能重复
|
||
"""
|
||
import json
|
||
import threading
|
||
import time
|
||
import traceback
|
||
from typing import Dict, Any, Optional, List
|
||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||
from urllib.parse import urlparse
|
||
from dataclasses import dataclass, asdict
|
||
from datetime import datetime
|
||
|
||
from unilabos.utils.log import logger
|
||
|
||
|
||
@dataclass
|
||
class WorkstationReportRequest:
|
||
"""统一工作站报送请求(基于LIMS协议规范)"""
|
||
token: str # 授权令牌
|
||
request_time: str # 请求时间,格式:2024-12-12 12:12:12.xxx
|
||
data: Dict[str, Any] # 报送数据
|
||
|
||
|
||
@dataclass
|
||
class MaterialUsage:
|
||
"""物料使用记录"""
|
||
materialId: str # 物料Id(GUID)
|
||
locationId: str # 库位Id(GUID)
|
||
typeMode: str # 物料类型(样品1、试剂2、耗材0)
|
||
usedQuantity: float # 使用的数量(数字)
|
||
|
||
|
||
@dataclass
|
||
class HttpResponse:
|
||
"""HTTP响应"""
|
||
success: bool
|
||
message: str
|
||
data: Optional[Dict[str, Any]] = None
|
||
acknowledgment_id: Optional[str] = None
|
||
|
||
|
||
class WorkstationHTTPHandler(BaseHTTPRequestHandler):
|
||
"""工作站HTTP请求处理器"""
|
||
|
||
def __init__(self, workstation_instance, *args, **kwargs):
|
||
self.workstation = workstation_instance
|
||
super().__init__(*args, **kwargs)
|
||
|
||
def do_POST(self):
|
||
"""处理POST请求 - 统一的工作站报送接口"""
|
||
try:
|
||
# 解析请求路径
|
||
parsed_path = urlparse(self.path)
|
||
endpoint = parsed_path.path
|
||
|
||
# 读取请求体
|
||
content_length = int(self.headers.get('Content-Length', 0))
|
||
if content_length > 0:
|
||
post_data = self.rfile.read(content_length)
|
||
request_data = json.loads(post_data.decode('utf-8'))
|
||
else:
|
||
request_data = {}
|
||
|
||
logger.info(f"收到工作站报送: {endpoint} - {request_data.get('token', 'unknown')}")
|
||
|
||
# 统一的报送端点路由(基于LIMS协议规范)
|
||
if endpoint == '/report/step_finish':
|
||
response = self._handle_step_finish_report(request_data)
|
||
elif endpoint == '/report/sample_finish':
|
||
response = self._handle_sample_finish_report(request_data)
|
||
elif endpoint == '/report/order_finish':
|
||
response = self._handle_order_finish_report(request_data)
|
||
elif endpoint == '/report/batch_update':
|
||
response = self._handle_batch_update_report(request_data)
|
||
# 扩展报送端点
|
||
elif endpoint == '/report/material_change':
|
||
response = self._handle_material_change_report(request_data)
|
||
elif endpoint == '/report/error_handling':
|
||
response = self._handle_error_handling_report(request_data)
|
||
# 保留LIMS协议端点以兼容现有系统
|
||
elif endpoint == '/LIMS/step_finish':
|
||
response = self._handle_step_finish_report(request_data)
|
||
elif endpoint == '/LIMS/preintake_finish':
|
||
response = self._handle_sample_finish_report(request_data)
|
||
elif endpoint == '/LIMS/order_finish':
|
||
response = self._handle_order_finish_report(request_data)
|
||
else:
|
||
response = HttpResponse(
|
||
success=False,
|
||
message=f"不支持的报送端点: {endpoint}",
|
||
data={"supported_endpoints": [
|
||
"/report/step_finish",
|
||
"/report/sample_finish",
|
||
"/report/order_finish",
|
||
"/report/batch_update",
|
||
"/report/material_change",
|
||
"/report/error_handling"
|
||
]}
|
||
)
|
||
|
||
# 发送响应
|
||
self._send_response(response)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理工作站报送失败: {e}\\n{traceback.format_exc()}")
|
||
error_response = HttpResponse(
|
||
success=False,
|
||
message=f"请求处理失败: {str(e)}"
|
||
)
|
||
self._send_response(error_response)
|
||
|
||
def do_GET(self):
|
||
"""处理GET请求 - 健康检查和状态查询"""
|
||
try:
|
||
parsed_path = urlparse(self.path)
|
||
endpoint = parsed_path.path
|
||
|
||
if endpoint == '/status':
|
||
response = self._handle_status_check()
|
||
elif endpoint == '/health':
|
||
response = HttpResponse(success=True, message="服务健康")
|
||
else:
|
||
response = HttpResponse(
|
||
success=False,
|
||
message=f"不支持的查询端点: {endpoint}",
|
||
data={"supported_endpoints": ["/status", "/health"]}
|
||
)
|
||
|
||
self._send_response(response)
|
||
|
||
except Exception as e:
|
||
logger.error(f"GET请求处理失败: {e}")
|
||
error_response = HttpResponse(
|
||
success=False,
|
||
message=f"GET请求处理失败: {str(e)}"
|
||
)
|
||
self._send_response(error_response)
|
||
|
||
def do_OPTIONS(self):
|
||
"""处理OPTIONS请求 - CORS预检请求"""
|
||
try:
|
||
# 发送CORS响应头
|
||
self.send_response(200)
|
||
self.send_header('Access-Control-Allow-Origin', '*')
|
||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
|
||
self.send_header('Access-Control-Max-Age', '86400')
|
||
self.end_headers()
|
||
|
||
except Exception as e:
|
||
logger.error(f"OPTIONS请求处理失败: {e}")
|
||
self.send_response(500)
|
||
self.end_headers()
|
||
|
||
def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||
"""处理步骤完成报送(统一LIMS协议规范)"""
|
||
try:
|
||
# 验证基本字段
|
||
required_fields = ['token', 'request_time', 'data']
|
||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||
)
|
||
|
||
# 验证data字段内容
|
||
data = request_data['data']
|
||
data_required_fields = ['orderCode', 'orderName', 'stepName', 'stepId', 'sampleId', 'startTime', 'endTime']
|
||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||
)
|
||
|
||
# 创建统一请求对象
|
||
report_request = WorkstationReportRequest(
|
||
token=request_data['token'],
|
||
request_time=request_data['request_time'],
|
||
data=data
|
||
)
|
||
|
||
# 调用工作站处理方法
|
||
result = self.workstation.process_step_finish_report(report_request)
|
||
|
||
return HttpResponse(
|
||
success=True,
|
||
message=f"步骤完成报送已处理: {data['stepName']} ({data['orderCode']})",
|
||
acknowledgment_id=f"STEP_{int(time.time() * 1000)}_{data['stepId']}",
|
||
data=result
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理步骤完成报送失败: {e}")
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"步骤完成报送处理失败: {str(e)}"
|
||
)
|
||
|
||
def _handle_sample_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||
"""处理通量完成报送(统一LIMS协议规范)"""
|
||
try:
|
||
# 验证基本字段
|
||
required_fields = ['token', 'request_time', 'data']
|
||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||
)
|
||
|
||
# 验证data字段内容
|
||
data = request_data['data']
|
||
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'status']
|
||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||
)
|
||
|
||
# 创建统一请求对象
|
||
report_request = WorkstationReportRequest(
|
||
token=request_data['token'],
|
||
request_time=request_data['request_time'],
|
||
data=data
|
||
)
|
||
|
||
# 调用工作站处理方法
|
||
result = self.workstation.process_sample_finish_report(report_request)
|
||
|
||
status_names = {
|
||
"0": "待生产", "2": "进样", "10": "开始",
|
||
"20": "完成", "-2": "异常停止", "-3": "人工停止"
|
||
}
|
||
status_desc = status_names.get(str(data['status']), f"状态{data['status']}")
|
||
|
||
return HttpResponse(
|
||
success=True,
|
||
message=f"通量完成报送已处理: {data['sampleId']} ({data['orderCode']}) - {status_desc}",
|
||
acknowledgment_id=f"SAMPLE_{int(time.time() * 1000)}_{data['sampleId']}",
|
||
data=result
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理通量完成报送失败: {e}")
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"通量完成报送处理失败: {str(e)}"
|
||
)
|
||
|
||
def _handle_order_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||
"""处理任务完成报送(统一LIMS协议规范)"""
|
||
try:
|
||
# 验证基本字段
|
||
required_fields = ['token', 'request_time', 'data']
|
||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||
)
|
||
|
||
# 验证data字段内容
|
||
data = request_data['data']
|
||
data_required_fields = ['orderCode', 'orderName', 'startTime', 'endTime', 'status']
|
||
if data_missing_fields := [field for field in data_required_fields if field not in data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
|
||
)
|
||
|
||
# 处理物料使用记录
|
||
used_materials = []
|
||
if 'usedMaterials' in data:
|
||
for material_data in data['usedMaterials']:
|
||
material = MaterialUsage(
|
||
materialId=material_data.get('materialId', ''),
|
||
locationId=material_data.get('locationId', ''),
|
||
typeMode=material_data.get('typeMode', ''),
|
||
usedQuantity=material_data.get('usedQuantity', 0.0)
|
||
)
|
||
used_materials.append(material)
|
||
|
||
# 创建统一请求对象
|
||
report_request = WorkstationReportRequest(
|
||
token=request_data['token'],
|
||
request_time=request_data['request_time'],
|
||
data=data
|
||
)
|
||
|
||
# 调用工作站处理方法
|
||
result = self.workstation.process_order_finish_report(report_request, used_materials)
|
||
|
||
status_names = {"30": "完成", "-11": "异常停止", "-12": "人工停止"}
|
||
status_desc = status_names.get(str(data['status']), f"状态{data['status']}")
|
||
|
||
return HttpResponse(
|
||
success=True,
|
||
message=f"任务完成报送已处理: {data['orderName']} ({data['orderCode']}) - {status_desc}",
|
||
acknowledgment_id=f"ORDER_{int(time.time() * 1000)}_{data['orderCode']}",
|
||
data=result
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理任务完成报送失败: {e}")
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"任务完成报送处理失败: {str(e)}"
|
||
)
|
||
|
||
def _handle_batch_update_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||
"""处理批量报送"""
|
||
try:
|
||
step_updates = request_data.get('step_updates', [])
|
||
sample_updates = request_data.get('sample_updates', [])
|
||
order_updates = request_data.get('order_updates', [])
|
||
|
||
results = {
|
||
'step_results': [],
|
||
'sample_results': [],
|
||
'order_results': [],
|
||
'total_processed': 0,
|
||
'total_failed': 0
|
||
}
|
||
|
||
# 处理批量步骤更新
|
||
for step_data in step_updates:
|
||
try:
|
||
step_data['token'] = request_data.get('token', step_data.get('token'))
|
||
step_data['request_time'] = request_data.get('request_time', step_data.get('request_time'))
|
||
result = self._handle_step_finish_report(step_data)
|
||
results['step_results'].append(result)
|
||
if result.success:
|
||
results['total_processed'] += 1
|
||
else:
|
||
results['total_failed'] += 1
|
||
except Exception as e:
|
||
results['step_results'].append(HttpResponse(success=False, message=str(e)))
|
||
results['total_failed'] += 1
|
||
|
||
# 处理批量通量更新
|
||
for sample_data in sample_updates:
|
||
try:
|
||
sample_data['token'] = request_data.get('token', sample_data.get('token'))
|
||
sample_data['request_time'] = request_data.get('request_time', sample_data.get('request_time'))
|
||
result = self._handle_sample_finish_report(sample_data)
|
||
results['sample_results'].append(result)
|
||
if result.success:
|
||
results['total_processed'] += 1
|
||
else:
|
||
results['total_failed'] += 1
|
||
except Exception as e:
|
||
results['sample_results'].append(HttpResponse(success=False, message=str(e)))
|
||
results['total_failed'] += 1
|
||
|
||
# 处理批量任务更新
|
||
for order_data in order_updates:
|
||
try:
|
||
order_data['token'] = request_data.get('token', order_data.get('token'))
|
||
order_data['request_time'] = request_data.get('request_time', order_data.get('request_time'))
|
||
result = self._handle_order_finish_report(order_data)
|
||
results['order_results'].append(result)
|
||
if result.success:
|
||
results['total_processed'] += 1
|
||
else:
|
||
results['total_failed'] += 1
|
||
except Exception as e:
|
||
results['order_results'].append(HttpResponse(success=False, message=str(e)))
|
||
results['total_failed'] += 1
|
||
|
||
return HttpResponse(
|
||
success=results['total_failed'] == 0,
|
||
message=f"批量报送处理完成: {results['total_processed']} 成功, {results['total_failed']} 失败",
|
||
acknowledgment_id=f"BATCH_{int(time.time() * 1000)}",
|
||
data=results
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理批量报送失败: {e}")
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"批量报送处理失败: {str(e)}"
|
||
)
|
||
|
||
def _handle_material_change_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||
"""处理物料变更报送"""
|
||
try:
|
||
# 验证必需字段
|
||
if 'brand' in request_data:
|
||
if request_data['brand'] == "bioyond": # 奔曜
|
||
error_msg = request_data["text"]
|
||
logger.info(f"收到奔曜错误处理报送: {error_msg}")
|
||
return HttpResponse(
|
||
success=True,
|
||
message=f"错误处理报送已收到: {error_msg}",
|
||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}",
|
||
data=None
|
||
)
|
||
else:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"缺少厂家信息(brand字段)"
|
||
)
|
||
required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type']
|
||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||
)
|
||
|
||
# 调用工作站的处理方法
|
||
result = self.workstation.process_material_change_report(request_data)
|
||
|
||
return HttpResponse(
|
||
success=True,
|
||
message=f"物料变更报送已处理: {request_data['resource_id']} ({request_data['change_type']})",
|
||
acknowledgment_id=f"MATERIAL_{int(time.time() * 1000)}_{request_data['resource_id']}",
|
||
data=result
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理物料变更报送失败: {e}")
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"物料变更报送处理失败: {str(e)}"
|
||
)
|
||
|
||
def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse:
|
||
"""处理错误处理报送"""
|
||
try:
|
||
# 检查是否为奔曜格式的错误报送
|
||
if 'brand' in request_data and str(request_data['brand']).lower() == "bioyond":
|
||
# 奔曜格式处理
|
||
if 'text' not in request_data:
|
||
return HttpResponse(
|
||
success=False,
|
||
message="奔曜格式缺少text字段"
|
||
)
|
||
|
||
error_data = request_data["text"]
|
||
logger.info(f"收到奔曜错误处理报送: {error_data}")
|
||
|
||
# 调用工作站的处理方法
|
||
result = self.workstation.handle_external_error(error_data)
|
||
|
||
return HttpResponse(
|
||
success=True,
|
||
message=f"错误处理报送已收到: 任务{error_data.get('task', 'unknown')}, 错误代码{error_data.get('code', 'unknown')}",
|
||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_data.get('task', 'unknown')}",
|
||
data=result
|
||
)
|
||
else:
|
||
# 标准格式处理
|
||
required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message']
|
||
if missing_fields := [field for field in required_fields if field not in request_data]:
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"缺少必要字段: {', '.join(missing_fields)}"
|
||
)
|
||
|
||
# 调用工作站的处理方法
|
||
result = self.workstation.handle_external_error(request_data)
|
||
|
||
return HttpResponse(
|
||
success=True,
|
||
message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}",
|
||
acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}",
|
||
data=result
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"处理错误处理报送失败: {e}")
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"错误处理报送处理失败: {str(e)}"
|
||
)
|
||
|
||
def _handle_status_check(self) -> HttpResponse:
|
||
"""处理状态查询"""
|
||
try:
|
||
# 安全地获取 device_id
|
||
device_id = "unknown"
|
||
if hasattr(self.workstation, 'device_id'):
|
||
device_id = self.workstation.device_id
|
||
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
|
||
device_id = self.workstation._ros_node.device_id
|
||
|
||
return HttpResponse(
|
||
success=True,
|
||
message="工作站报送服务正常运行",
|
||
data={
|
||
"workstation_id": device_id,
|
||
"service_type": "unified_reporting_service",
|
||
"uptime": time.time() - getattr(self.workstation, '_start_time', time.time()),
|
||
"reports_received": getattr(self.workstation, '_reports_received_count', 0),
|
||
"supported_endpoints": [
|
||
"POST /report/step_finish",
|
||
"POST /report/sample_finish",
|
||
"POST /report/order_finish",
|
||
"POST /report/batch_update",
|
||
"POST /report/material_change",
|
||
"POST /report/error_handling",
|
||
"GET /status",
|
||
"GET /health"
|
||
]
|
||
}
|
||
)
|
||
except Exception as e:
|
||
logger.error(f"处理状态查询失败: {e}")
|
||
return HttpResponse(
|
||
success=False,
|
||
message=f"状态查询失败: {str(e)}"
|
||
)
|
||
|
||
def _send_response(self, response: HttpResponse):
|
||
"""发送响应"""
|
||
try:
|
||
# 设置响应状态码
|
||
status_code = 200 if response.success else 400
|
||
self.send_response(status_code)
|
||
|
||
# 设置响应头
|
||
self.send_header('Content-Type', 'application/json; charset=utf-8')
|
||
self.send_header('Access-Control-Allow-Origin', '*')
|
||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||
self.end_headers()
|
||
|
||
# 发送响应体
|
||
response_json = json.dumps(asdict(response), ensure_ascii=False, indent=2)
|
||
self.wfile.write(response_json.encode('utf-8'))
|
||
|
||
except Exception as e:
|
||
logger.error(f"发送响应失败: {e}")
|
||
|
||
def log_message(self, format, *args):
|
||
"""重写日志方法"""
|
||
logger.debug(f"HTTP请求: {format % args}")
|
||
|
||
|
||
class WorkstationHTTPService:
|
||
"""工作站HTTP服务"""
|
||
|
||
def __init__(self, workstation_instance, host: str = "127.0.0.1", port: int = 8080):
|
||
self.workstation = workstation_instance
|
||
self.host = host
|
||
self.port = port
|
||
self.server = None
|
||
self.server_thread = None
|
||
self.running = False
|
||
|
||
# 初始化统计信息
|
||
self.workstation._start_time = time.time()
|
||
self.workstation._reports_received_count = 0
|
||
|
||
def start(self):
|
||
"""启动HTTP服务"""
|
||
try:
|
||
# 创建处理器工厂函数
|
||
def handler_factory(*args, **kwargs):
|
||
return WorkstationHTTPHandler(self.workstation, *args, **kwargs)
|
||
|
||
# 创建HTTP服务器
|
||
self.server = HTTPServer((self.host, self.port), handler_factory)
|
||
|
||
# 安全地获取 device_id 用于线程命名
|
||
device_id = "unknown"
|
||
if hasattr(self.workstation, 'device_id'):
|
||
device_id = self.workstation.device_id
|
||
elif hasattr(self.workstation, '_ros_node') and hasattr(self.workstation._ros_node, 'device_id'):
|
||
device_id = self.workstation._ros_node.device_id
|
||
|
||
# 在单独线程中运行服务器
|
||
self.server_thread = threading.Thread(
|
||
target=self._run_server,
|
||
daemon=True,
|
||
name=f"WorkstationHTTP-{device_id}"
|
||
)
|
||
|
||
self.running = True
|
||
self.server_thread.start()
|
||
|
||
logger.info(f"工作站HTTP报送服务已启动: http://{self.host}:{self.port}")
|
||
logger.info("统一的报送端点 (基于LIMS协议规范):")
|
||
logger.info(" - POST /report/step_finish # 步骤完成报送")
|
||
logger.info(" - POST /report/sample_finish # 通量完成报送")
|
||
logger.info(" - POST /report/order_finish # 任务完成报送")
|
||
logger.info(" - POST /report/batch_update # 批量更新报送")
|
||
logger.info("扩展报送端点:")
|
||
logger.info(" - POST /report/material_change # 物料变更报送")
|
||
logger.info(" - POST /report/error_handling # 错误处理报送")
|
||
logger.info("兼容端点:")
|
||
logger.info(" - POST /LIMS/step_finish # 兼容LIMS步骤完成")
|
||
logger.info(" - POST /LIMS/preintake_finish # 兼容LIMS通量完成")
|
||
logger.info(" - POST /LIMS/order_finish # 兼容LIMS任务完成")
|
||
logger.info("服务端点:")
|
||
logger.info(" - GET /status # 服务状态查询")
|
||
logger.info(" - GET /health # 健康检查")
|
||
|
||
except Exception as e:
|
||
logger.error(f"启动HTTP服务失败: {e}")
|
||
raise
|
||
|
||
def stop(self):
|
||
"""停止HTTP服务"""
|
||
try:
|
||
if self.running and self.server:
|
||
logger.info("正在停止工作站HTTP报送服务...")
|
||
self.running = False
|
||
|
||
# 停止serve_forever循环
|
||
self.server.shutdown()
|
||
|
||
# 等待服务器线程结束
|
||
if self.server_thread and self.server_thread.is_alive():
|
||
self.server_thread.join(timeout=5.0)
|
||
|
||
# 关闭服务器套接字
|
||
self.server.server_close()
|
||
|
||
logger.info("工作站HTTP报送服务已停止")
|
||
|
||
except Exception as e:
|
||
logger.error(f"停止HTTP服务失败: {e}")
|
||
|
||
def _run_server(self):
|
||
"""运行HTTP服务器"""
|
||
try:
|
||
# 使用serve_forever()让服务持续运行
|
||
self.server.serve_forever()
|
||
except Exception as e:
|
||
if self.running: # 只在非正常停止时记录错误
|
||
logger.error(f"HTTP服务运行错误: {e}")
|
||
finally:
|
||
logger.info("HTTP服务器线程已退出")
|
||
|
||
@property
|
||
def is_running(self) -> bool:
|
||
"""检查服务是否正在运行"""
|
||
return self.running and self.server_thread and self.server_thread.is_alive()
|
||
|
||
@property
|
||
def service_url(self) -> str:
|
||
"""获取服务URL"""
|
||
return f"http://{self.host}:{self.port}"
|
||
|
||
|
||
# 导出主要类 - 保持向后兼容
|
||
@dataclass
|
||
class MaterialChangeReport:
|
||
"""已废弃:物料变更报送,请使用统一的WorkstationReportRequest"""
|
||
pass
|
||
|
||
|
||
@dataclass
|
||
class TaskExecutionReport:
|
||
"""已废弃:任务执行报送,请使用统一的WorkstationReportRequest"""
|
||
pass
|
||
|
||
|
||
# 导出列表
|
||
__all__ = [
|
||
'WorkstationReportRequest',
|
||
'MaterialUsage',
|
||
'HttpResponse',
|
||
'WorkstationHTTPService',
|
||
# 向后兼容
|
||
'MaterialChangeReport',
|
||
'TaskExecutionReport'
|
||
]
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 简单测试HTTP服务
|
||
class BioyondWorkstation:
|
||
device_id = "WS-001"
|
||
|
||
def process_step_finish_report(self, report_request):
|
||
return {"processed": True}
|
||
|
||
def process_sample_finish_report(self, report_request):
|
||
return {"processed": True}
|
||
|
||
def process_order_finish_report(self, report_request, used_materials):
|
||
return {"processed": True}
|
||
|
||
def process_material_change_report(self, report_data):
|
||
return {"processed": True}
|
||
|
||
def handle_external_error(self, error_data):
|
||
return {"handled": True}
|
||
|
||
workstation = BioyondWorkstation()
|
||
http_service = WorkstationHTTPService(workstation)
|
||
|
||
try:
|
||
http_service.start()
|
||
print(f"测试服务器已启动: {http_service.service_url}")
|
||
print("按 Ctrl+C 停止服务器")
|
||
print("服务将持续运行,等待接收HTTP请求...")
|
||
|
||
# 保持服务器运行 - 使用更好的等待机制
|
||
try:
|
||
while http_service.is_running:
|
||
time.sleep(1)
|
||
except KeyboardInterrupt:
|
||
print("\n接收到停止信号...")
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n正在停止服务器...")
|
||
http_service.stop()
|
||
print("服务器已停止")
|
||
except Exception as e:
|
||
print(f"服务器运行错误: {e}")
|
||
http_service.stop()
|
||
|