HR物料同步,前端展示位置修复 (#135)

* 更新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>
This commit is contained in:
ZiWei
2025-11-15 02:57:48 +08:00
committed by GitHub
parent d869c14233
commit 850eeae55a
19 changed files with 2578 additions and 585 deletions

View File

@@ -212,8 +212,14 @@ class BioyondV1RPC(BaseRequest):
})
if not response or response['code'] != 1:
if response:
error_msg = response.get('message', '未知错误')
print(f"[ERROR] 物料入库失败: code={response.get('code')}, message={error_msg}")
else:
print(f"[ERROR] 物料入库失败: API 无响应")
return {}
return response.get("data", {})
# 入库成功时,即使没有 data 字段,也返回成功标识
return response.get("data") or {"success": True}
def delete_material(self, material_id: str) -> dict:
"""
@@ -501,7 +507,7 @@ class BioyondV1RPC(BaseRequest):
return {}
response = self.post(
url=f'{self.host}/api/lims/order/order-report',
url=f'{self.host}/api/lims/order/project-order-report',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -678,7 +684,7 @@ class BioyondV1RPC(BaseRequest):
def scheduler_pause(self) -> int:
"""描述:暂停调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-pause',
url=f'{self.host}/api/lims/scheduler/pause',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -689,8 +695,9 @@ class BioyondV1RPC(BaseRequest):
return response.get("code", 0)
def scheduler_continue(self) -> int:
"""描述:继续调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-continue',
url=f'{self.host}/api/lims/scheduler/continue',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -703,7 +710,7 @@ class BioyondV1RPC(BaseRequest):
def scheduler_stop(self) -> int:
"""描述:停止调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-stop',
url=f'{self.host}/api/lims/scheduler/stop',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -714,9 +721,9 @@ class BioyondV1RPC(BaseRequest):
return response.get("code", 0)
def scheduler_reset(self) -> int:
"""描述:重置调度器"""
"""描述:复位调度器"""
response = self.post(
url=f'{self.host}/api/lims/scheduler/scheduler-reset',
url=f'{self.host}/api/lims/scheduler/reset',
params={
"apiKey": self.api_key,
"requestTime": self.get_current_time_iso8601(),
@@ -734,7 +741,7 @@ class BioyondV1RPC(BaseRequest):
print("正在加载材料列表缓存...")
# 加载所有类型的材料:耗材(0)、样品(1)、试剂(2)
material_types = [1, 2]
material_types = [0, 1, 2]
for type_mode in material_types:
print(f"正在加载类型 {type_mode} 的材料...")
@@ -788,4 +795,4 @@ class BioyondV1RPC(BaseRequest):
def get_available_materials(self):
"""获取所有可用的材料名称列表"""
return list(self.material_cache.keys())
return list(self.material_cache.keys())

View File

@@ -1,5 +1,7 @@
from datetime import datetime
import json
import time
from typing import Optional, Dict, Any
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
@@ -23,6 +25,9 @@ class BioyondDispensingStation(BioyondWorkstation):
# self._logger = SimpleLogger()
# self.is_running = False
# 用于跟踪任务完成状态的字典: {orderCode: {status, order_id, timestamp}}
self.order_completion_status = {}
# 90%10%小瓶投料任务创建方法
def create_90_10_vial_feeding_task(self,
order_name: str = None,
@@ -270,7 +275,45 @@ class BioyondDispensingStation(BioyondWorkstation):
# 7. 调用create_order方法创建任务
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
return json.dumps({"suc": True})
# 8. 解析结果获取order_id
order_id = None
if isinstance(result, str):
# result 格式: "{'3a1d895c-4d39-d504-1398-18f5a40bac1e': [{'id': '...', ...}]}"
# 第一个键就是order_id (UUID)
try:
# 尝试解析字符串为字典
import ast
result_dict = ast.literal_eval(result)
# 获取第一个键作为order_id
if result_dict and isinstance(result_dict, dict):
first_key = list(result_dict.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
else:
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
except Exception as e:
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}, result类型={type(result)}")
elif isinstance(result, dict):
# 如果已经是字典
if result:
first_key = list(result.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
if not order_id:
self.hardware_interface._logger.warning(
f"⚠ 未能提取order_idresult={result[:100] if isinstance(result, str) else result}"
)
# 返回成功结果和构建的JSON数据
return json.dumps({
"suc": True,
"order_code": order_code,
"order_id": order_id,
"result": result,
"order_params": order_data
})
except BioyondException:
# 重新抛出BioyondException
@@ -398,7 +441,37 @@ class BioyondDispensingStation(BioyondWorkstation):
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
return json.dumps({"suc": True})
# 8. 解析结果获取order_id
order_id = None
if isinstance(result, str):
try:
import ast
result_dict = ast.literal_eval(result)
if result_dict and isinstance(result_dict, dict):
first_key = list(result_dict.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id: {order_id}")
else:
self.hardware_interface._logger.warning(f"result_dict格式异常: {result_dict}")
except Exception as e:
self.hardware_interface._logger.error(f"✗ 无法从结果中提取order_id: {e}")
elif isinstance(result, dict):
if result:
first_key = list(result.keys())[0]
order_id = first_key
self.hardware_interface._logger.info(f"✓ 成功提取order_id(dict): {order_id}")
if not order_id:
self.hardware_interface._logger.warning(f"⚠ 未能提取order_id")
# 返回成功结果和构建的JSON数据
return json.dumps({
"suc": True,
"order_code": order_code,
"order_id": order_id,
"result": result,
"order_params": order_data
})
except BioyondException:
# 重新抛出BioyondException
@@ -499,15 +572,24 @@ class BioyondDispensingStation(BioyondWorkstation):
hold_m_name=hold_m_name
)
# 解析返回结果以获取order_code和order_id
result_data = json.loads(result) if isinstance(result, str) else result
order_code = result_data.get("order_code")
order_id = result_data.get("order_id")
order_params = result_data.get("order_params", {})
results.append({
"index": idx + 1,
"name": name,
"success": True,
"hold_m_name": hold_m_name
"order_code": order_code,
"order_id": order_id,
"hold_m_name": hold_m_name,
"order_params": order_params
})
success_count += 1
self.hardware_interface._logger.info(
f"成功创建二胺溶液配置任务: {name}"
f"成功创建二胺溶液配置任务: {name}, order_code={order_code}, order_id={order_id}"
)
except BioyondException as e:
@@ -533,11 +615,17 @@ class BioyondDispensingStation(BioyondWorkstation):
f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}"
)
# 提取所有成功任务的order_code和order_id
order_codes = [r["order_code"] for r in results if r["success"]]
order_ids = [r["order_id"] for r in results if r["success"]]
# 返回汇总结果
summary = {
"total": len(solutions),
"success": success_count,
"failed": failed_count,
"order_codes": order_codes,
"order_ids": order_ids,
"details": results
}
@@ -546,8 +634,13 @@ class BioyondDispensingStation(BioyondWorkstation):
f"成功={success_count}, 失败={failed_count}"
)
# 返回JSON字符串格式
return json.dumps(summary, ensure_ascii=False)
# 构建返回结果
summary["return_info"] = {
"order_codes": order_codes,
"order_ids": order_ids,
}
return summary
except BioyondException:
raise
@@ -613,22 +706,15 @@ class BioyondDispensingStation(BioyondWorkstation):
if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]):
raise BioyondException("titration 数据缺少必要参数")
# 将main_portion平均分成3份作为90%物料3个小瓶
portion_90 = main_portion / 3
# 调用单个任务创建方法
result = self.create_90_10_vial_feeding_task(
order_name=f"90%10%小瓶投料-{name}",
speed=speed,
temperature=temperature,
delay_time=delay_time,
# 90%物料 - 主称固体平均分成3份
# 90%物料 - 主称固体直接使用main_portion
percent_90_1_assign_material_name=name,
percent_90_1_target_weigh=str(round(portion_90, 6)),
percent_90_2_assign_material_name=name,
percent_90_2_target_weigh=str(round(portion_90, 6)),
percent_90_3_assign_material_name=name,
percent_90_3_target_weigh=str(round(portion_90, 6)),
percent_90_1_target_weigh=str(round(main_portion, 6)),
# 10%物料 - 滴定固体 + 滴定溶剂只使用第1个10%小瓶)
percent_10_1_assign_material_name=name,
percent_10_1_target_weigh=str(round(titration_portion, 6)),
@@ -637,29 +723,54 @@ class BioyondDispensingStation(BioyondWorkstation):
hold_m_name=hold_m_name
)
summary = {
# 解析返回结果以获取order_code和order_id
result_data = json.loads(result) if isinstance(result, str) else result
order_code = result_data.get("order_code")
order_id = result_data.get("order_id")
order_params = result_data.get("order_params", {})
# 构建详细信息(保持原有结构)
detail = {
"index": 1,
"name": name,
"success": True,
"order_code": order_code,
"order_id": order_id,
"hold_m_name": hold_m_name,
"material_name": name,
"90_vials": {
"count": 3,
"weight_per_vial": round(portion_90, 6),
"count": 1,
"weight_per_vial": round(main_portion, 6),
"total_weight": round(main_portion, 6)
},
"10_vials": {
"count": 1,
"solid_weight": round(titration_portion, 6),
"liquid_volume": round(titration_solvent, 6)
}
},
"order_params": order_params
}
# 构建批量结果格式与diamine_solution_tasks保持一致
summary = {
"total": 1,
"success": 1,
"failed": 0,
"order_codes": [order_code],
"order_ids": [order_id],
"details": [detail]
}
self.hardware_interface._logger.info(
f"成功创建90%10%小瓶投料任务: {hold_m_name}, "
f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL"
f"成功创建90%10%小瓶投料任务: {name}, order_code={order_code}, order_id={order_id}"
)
# 返回JSON字符串格式
return json.dumps(summary, ensure_ascii=False)
# 构建返回结果
summary["return_info"] = {
"order_codes": [order_code],
"order_ids": [order_id],
}
return summary
except BioyondException:
raise
@@ -669,6 +780,279 @@ class BioyondDispensingStation(BioyondWorkstation):
raise BioyondException(error_msg)
def wait_for_multiple_orders_and_get_reports(self,
batch_create_result: str = None,
timeout: int = 7200,
check_interval: int = 10) -> Dict[str, Any]:
"""
同时等待多个任务完成并获取实验报告
参数说明:
- batch_create_result: 批量创建任务的返回结果JSON字符串包含order_codes和order_ids数组
- timeout: 超时时间默认7200秒2小时
- check_interval: 检查间隔默认10秒
返回: 包含所有任务状态和报告的字典
{
"total": 2,
"completed": 2,
"timeout": 0,
"elapsed_time": 120.5,
"reports": [
{
"order_code": "task_vial_1",
"order_id": "uuid1",
"status": "completed",
"completion_status": 30,
"report": {...}
},
...
]
}
异常:
- BioyondException: 所有任务都超时或发生错误
"""
try:
# 参数类型转换
timeout = int(timeout) if timeout else 7200
check_interval = int(check_interval) if check_interval else 10
# 验证batch_create_result参数
if not batch_create_result or batch_create_result == "":
raise BioyondException("batch_create_result参数为空请确保从batch_create节点正确连接handle")
# 解析batch_create_result JSON对象
try:
# 清理可能存在的截断标记 [...]
if isinstance(batch_create_result, str) and '[...]' in batch_create_result:
batch_create_result = batch_create_result.replace('[...]', '[]')
result_obj = json.loads(batch_create_result) if isinstance(batch_create_result, str) else batch_create_result
# 兼容外层包装格式 {error, suc, return_value}
if isinstance(result_obj, dict) and "return_value" in result_obj:
inner = result_obj.get("return_value")
if isinstance(inner, str):
result_obj = json.loads(inner)
elif isinstance(inner, dict):
result_obj = inner
# 从summary对象中提取order_codes和order_ids
order_codes = result_obj.get("order_codes", [])
order_ids = result_obj.get("order_ids", [])
except json.JSONDecodeError as e:
raise BioyondException(f"解析batch_create_result失败: {e}")
except Exception as e:
raise BioyondException(f"处理batch_create_result时出错: {e}")
# 验证提取的数据
if not order_codes:
raise BioyondException("batch_create_result中未找到order_codes字段或为空")
if not order_ids:
raise BioyondException("batch_create_result中未找到order_ids字段或为空")
# 确保order_codes和order_ids是列表类型
if not isinstance(order_codes, list):
order_codes = [order_codes] if order_codes else []
if not isinstance(order_ids, list):
order_ids = [order_ids] if order_ids else []
codes_list = order_codes
ids_list = order_ids
if len(codes_list) != len(ids_list):
raise BioyondException(
f"order_codes数量({len(codes_list)})与order_ids数量({len(ids_list)})不匹配"
)
if not codes_list or not ids_list:
raise BioyondException("order_codes和order_ids不能为空")
# 初始化跟踪变量
total = len(codes_list)
pending_orders = {code: {"order_id": ids_list[i], "completed": False}
for i, code in enumerate(codes_list)}
reports = []
start_time = time.time()
self.hardware_interface._logger.info(
f"开始等待 {total} 个任务完成: {', '.join(codes_list)}"
)
# 轮询检查任务状态
while pending_orders:
elapsed_time = time.time() - start_time
# 检查超时
if elapsed_time > timeout:
# 收集超时任务
timeout_orders = list(pending_orders.keys())
self.hardware_interface._logger.error(
f"等待任务完成超时,剩余未完成任务: {', '.join(timeout_orders)}"
)
# 为超时任务添加记录
for order_code in timeout_orders:
reports.append({
"order_code": order_code,
"order_id": pending_orders[order_code]["order_id"],
"status": "timeout",
"completion_status": None,
"report": None,
"elapsed_time": elapsed_time
})
break
# 检查每个待完成的任务
completed_in_this_round = []
for order_code in list(pending_orders.keys()):
order_id = pending_orders[order_code]["order_id"]
# 检查任务是否完成
if order_code in self.order_completion_status:
completion_info = self.order_completion_status[order_code]
self.hardware_interface._logger.info(
f"检测到任务 {order_code} 已完成,状态: {completion_info.get('status')}"
)
# 获取实验报告
try:
report_query = json.dumps({"order_id": order_id})
report = self.hardware_interface.order_report(report_query)
if not report:
self.hardware_interface._logger.warning(
f"任务 {order_code} 已完成但无法获取报告"
)
report = {"error": "无法获取报告"}
else:
self.hardware_interface._logger.info(
f"成功获取任务 {order_code} 的实验报告"
)
reports.append({
"order_code": order_code,
"order_id": order_id,
"status": "completed",
"completion_status": completion_info.get('status'),
"report": report,
"elapsed_time": elapsed_time
})
# 标记为已完成
completed_in_this_round.append(order_code)
# 清理完成状态记录
del self.order_completion_status[order_code]
except Exception as e:
self.hardware_interface._logger.error(
f"查询任务 {order_code} 报告失败: {str(e)}"
)
reports.append({
"order_code": order_code,
"order_id": order_id,
"status": "error",
"completion_status": completion_info.get('status'),
"report": None,
"error": str(e),
"elapsed_time": elapsed_time
})
completed_in_this_round.append(order_code)
# 从待完成列表中移除已完成的任务
for order_code in completed_in_this_round:
del pending_orders[order_code]
# 如果还有待完成的任务,等待后继续
if pending_orders:
time.sleep(check_interval)
# 每分钟记录一次等待状态
new_elapsed_time = time.time() - start_time
if int(new_elapsed_time) % 60 == 0 and new_elapsed_time > 0:
self.hardware_interface._logger.info(
f"批量等待任务中... 已完成 {len(reports)}/{total}, "
f"待完成: {', '.join(pending_orders.keys())}, "
f"已等待 {int(new_elapsed_time/60)} 分钟"
)
# 统计结果
completed_count = sum(1 for r in reports if r['status'] == 'completed')
timeout_count = sum(1 for r in reports if r['status'] == 'timeout')
error_count = sum(1 for r in reports if r['status'] == 'error')
final_elapsed_time = time.time() - start_time
summary = {
"total": total,
"completed": completed_count,
"timeout": timeout_count,
"error": error_count,
"elapsed_time": round(final_elapsed_time, 2),
"reports": reports
}
self.hardware_interface._logger.info(
f"批量等待任务完成: 总数={total}, 成功={completed_count}, "
f"超时={timeout_count}, 错误={error_count}, 耗时={final_elapsed_time:.1f}"
)
# 返回字典格式,在顶层包含统计信息
return {
"return_info": json.dumps(summary, ensure_ascii=False)
}
except BioyondException:
raise
except Exception as e:
error_msg = f"批量等待任务完成时发生未预期的错误: {str(e)}"
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
def process_order_finish_report(self, report_request, used_materials) -> Dict[str, Any]:
"""
重写父类方法,处理任务完成报送并记录到 order_completion_status
Args:
report_request: WorkstationReportRequest 对象,包含任务完成信息
used_materials: 物料使用记录列表
Returns:
Dict[str, Any]: 处理结果
"""
try:
# 调用父类方法
result = super().process_order_finish_report(report_request, used_materials)
# 记录任务完成状态
data = report_request.data
order_code = data.get('orderCode')
if order_code:
self.order_completion_status[order_code] = {
'status': data.get('status'),
'order_name': data.get('orderName'),
'timestamp': datetime.now().isoformat(),
'start_time': data.get('startTime'),
'end_time': data.get('endTime')
}
self.hardware_interface._logger.info(
f"已记录任务完成状态: {order_code}, status={data.get('status')}"
)
return result
except Exception as e:
self.hardware_interface._logger.error(f"处理任务完成报送失败: {e}")
return {"processed": False, "error": str(e)}
if __name__ == "__main__":
bioyond = BioyondDispensingStation(config={
"api_key": "DE9BDDA0",
@@ -1089,4 +1473,3 @@ if __name__ == "__main__":
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
# bioyond.sample_waste_removal(id)

View File

@@ -345,7 +345,7 @@ class BioyondReactionStation(BioyondWorkstation):
def liquid_feeding_beaker(
self,
volume: str = "35000",
volume: str = "350",
assign_material_name: str = "BAPP",
time: str = "0",
torque_variation: int = 1,
@@ -355,7 +355,7 @@ class BioyondReactionStation(BioyondWorkstation):
"""液体进料烧杯
Args:
volume: 分液量(μL)
volume: 分液量(g)
assign_material_name: 物料名称(试剂瓶位)
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
@@ -580,7 +580,14 @@ class BioyondReactionStation(BioyondWorkstation):
# print(f"\n✅ 任务创建成功: {result}")
# print(f"\n✅ 任务创建成功")
print(f"{'='*60}\n")
return json.dumps({"success": True, "result": result})
# 返回结果,包含合并后的工作流数据和订单参数
return json.dumps({
"success": True,
"result": result,
"merged_workflow": merged_workflow,
"order_params": order_params
})
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
"""
@@ -780,4 +787,4 @@ class BioyondReactionStation(BioyondWorkstation):
except Exception as e:
print(f" ❌ 工作流ID验证失败: {e}")
print(f" 💡 将重新合并工作流")
return False
return False

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ Workstation HTTP Service Module
统一的工作站报送接收服务基于LIMS协议规范
1. 步骤完成报送 - POST /report/step_finish
2. 通量完成报送 - POST /report/sample_finish
2. 通量完成报送 - POST /report/sample_finish
3. 任务完成报送 - POST /report/order_finish
4. 批量更新报送 - POST /report/batch_update
5. 物料变更报送 - POST /report/material_change
@@ -54,18 +54,18 @@ class HttpResponse:
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:
@@ -73,9 +73,9 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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)
@@ -102,18 +102,18 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
success=False,
message=f"不支持的报送端点: {endpoint}",
data={"supported_endpoints": [
"/report/step_finish",
"/report/sample_finish",
"/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(
@@ -121,13 +121,13 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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':
@@ -138,9 +138,9 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
message=f"不支持的查询端点: {endpoint}",
data={"supported_endpoints": ["/status", "/health"]}
)
self._send_response(response)
except Exception as e:
logger.error(f"GET请求处理失败: {e}")
error_response = HttpResponse(
@@ -148,7 +148,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
message=f"GET请求处理失败: {str(e)}"
)
self._send_response(error_response)
def do_OPTIONS(self):
"""处理OPTIONS请求 - CORS预检请求"""
try:
@@ -159,12 +159,12 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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:
@@ -175,7 +175,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
success=False,
message=f"缺少必要字段: {', '.join(missing_fields)}"
)
# 验证data字段内容
data = request_data['data']
data_required_fields = ['orderCode', 'orderName', 'stepName', 'stepId', 'sampleId', 'startTime', 'endTime']
@@ -184,31 +184,31 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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:
@@ -219,7 +219,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
success=False,
message=f"缺少必要字段: {', '.join(missing_fields)}"
)
# 验证data字段内容
data = request_data['data']
data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'status']
@@ -228,37 +228,37 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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": "开始",
"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:
@@ -269,7 +269,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
success=False,
message=f"缺少必要字段: {', '.join(missing_fields)}"
)
# 验证data字段内容
data = request_data['data']
data_required_fields = ['orderCode', 'orderName', 'startTime', 'endTime', 'status']
@@ -278,7 +278,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
success=False,
message=f"data字段缺少必要内容: {', '.join(data_missing_fields)}"
)
# 处理物料使用记录
used_materials = []
if 'usedMaterials' in data:
@@ -290,41 +290,41 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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': [],
@@ -332,7 +332,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
'total_processed': 0,
'total_failed': 0
}
# 处理批量步骤更新
for step_data in step_updates:
try:
@@ -347,7 +347,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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:
@@ -362,7 +362,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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:
@@ -377,21 +377,21 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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:
@@ -417,24 +417,24 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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:
@@ -446,13 +446,13 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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')}",
@@ -467,38 +467,45 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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": self.workstation.device_id,
"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/sample_finish",
"POST /report/order_finish",
"POST /report/batch_update",
"POST /report/material_change",
@@ -514,28 +521,28 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
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}")
@@ -543,7 +550,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler):
class WorkstationHTTPService:
"""工作站HTTP服务"""
def __init__(self, workstation_instance, host: str = "127.0.0.1", port: int = 8080):
self.workstation = workstation_instance
self.host = host
@@ -551,31 +558,38 @@ class WorkstationHTTPService:
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-{self.workstation.device_id}"
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 # 步骤完成报送")
@@ -592,33 +606,33 @@ class WorkstationHTTPService:
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:
@@ -629,12 +643,12 @@ class WorkstationHTTPService:
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"""
@@ -648,7 +662,7 @@ class MaterialChangeReport:
pass
@dataclass
@dataclass
class TaskExecutionReport:
"""已废弃任务执行报送请使用统一的WorkstationReportRequest"""
pass
@@ -670,38 +684,38 @@ 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 = DummyWorkstation()
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()