Compare commits

..

21 Commits

Author SHA1 Message Date
Xuwznln
22ed7efa64 Merge branch 'dev' into fork/KCFeng425/fix-protocol-parameter
# Conflicts:
#	unilabos/app/mq.py
#	unilabos/registry/devices/virtual_device.yaml
#	unilabos/registry/devices/work_station.yaml
2025-07-16 11:10:05 +08:00
KCFeng425
ed3b22a738 补充了剩下的几个protocol 2025-07-16 10:38:12 +08:00
Xuwznln
540c5e94b7 增加注册表版本参数,支持将auto-指令人工检查后非auto,不生成人工已检查的指令,取消不必要的description生成 2025-07-16 09:46:32 +08:00
Xuwznln
f9aae44174 增加注册表版本参数,支持将auto-指令人工检查后非auto,不生成人工已检查的指令,取消不必要的description生成 2025-07-16 01:05:16 +08:00
Xuwznln
10cb645191 不生成已配置action的动作,增加prcxi的debug模式 2025-07-15 13:56:34 +08:00
Guangxin Zhang
4456529cfb Update prcxi.py to fit the function in unilabos. 2025-07-14 15:23:31 +08:00
Xuwznln
694a779c66 update prcxi registry 2025-07-11 18:43:11 +08:00
Xuwznln
5d214ebcd8 update prcxi 2025-07-11 18:20:50 +08:00
Xuwznln
0e11dacead assert blending_times > 0 2025-07-11 18:15:41 +08:00
Xuwznln
7b68545db3 prcxi blending 2025-07-11 18:11:44 +08:00
Guangxin Zhang
25960c2ed5 Add plateT6 to PRCXI configuration and enhance error handling in liquid handling 2025-07-11 18:10:21 +08:00
Xuwznln
72c67ba25c 任意执行错误都应该返回failed 2025-07-11 16:33:55 +08:00
Xuwznln
cd9e7ef12c 任意执行错误都应该返回failed 2025-07-11 16:30:56 +08:00
Xuwznln
b85722f44d add pickup tips for prcxi 2025-07-11 16:09:53 +08:00
Guangxin Zhang
5a2cc2d709 更新PRCXI配置,修改主机地址和设置状态,并添加示例用法 2025-07-11 16:00:45 +08:00
Xuwznln
644feced55 修正prcxi启动 2025-07-11 15:50:30 +08:00
Xuwznln
61ee446542 更新实例 2025-07-11 15:42:01 +08:00
Xuwznln
18f6685e18 更新实例 2025-07-11 15:36:46 +08:00
Xuwznln
e2052d4a2c 更新实例 2025-07-11 15:11:44 +08:00
q434343
50282664e0 修改moveit_interface,并在mqtt上报时发送一个时间戳,方便网页端对数据的筛选 (#62)
* Update README and MQTTClient for installation instructions and code improvements

* feat: 支持local_config启动
add: 增加对crt path的说明,为传入config.py的相对路径
move: web component

* add: registry description

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* feat: node_info_update srv
fix: OTDeck cant create

* close #12
feat: slave node registry

* feat: show machine name
fix: host node registry not uploaded

* feat: add hplc registry

* feat: add hplc registry

* fix: hplc status typo

* fix: devices/

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* fix: device.class possible null

* fix: HPLC additions with online service

* fix: slave mode spin not working

* fix: slave mode spin not working

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* feat: 多ProtocolNode 允许子设备ID相同
feat: 上报发现的ActionClient
feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报

* feat: 支持env设置config

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* Device visualization (#14)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>

* fix: missing hostname in devices_names
fix: upload_file for model file

* fix: missing paho-mqtt package
bump version to 0.9.0

* fix startup
add ResourceCreateFromOuter.action

* fix type hint

* update actions

* update actions

* host node add_resource_from_outer
fix cmake list

* pass device config to device class

* add: bind_parent_ids to resource create action
fix: message convert string

* fix: host node should not be re_discovered

* feat: resource tracker support dict

* feat: add more necessary params

* feat: fix boolean null in registry action data

* feat: add outer resource

* 编写mesh添加action

* feat: append resource

* add action

* feat: vis 2d for plr

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

* Device visualization (#22)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* 编写mesh添加action

* add action

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>

* fix: multi channel

* fix: aspirate

* fix: aspirate

* fix: aspirate

* fix: aspirate

* 提交

* fix: jobadd

* fix: jobadd

* fix: msg converter

* tijiao

* add resource creat easy action

* identify debug msg

* mq client id

* 提取lh的joint发布

* unify liquid_handler definition

* 修改物料跟随与物料添加逻辑

修改物料跟随与物料添加逻辑
将joint_publisher类移出lh的backends,但仍需要对lh的backends进行一些改写

* Revert "修改物料跟随与物料添加逻辑"

This reverts commit 498c997ad7.

* Reapply "修改物料跟随与物料添加逻辑"

This reverts commit 3a60d2ae81.

* Revert "Merge remote-tracking branch 'upstream/dev' into device_visualization"

This reverts commit fa727220af, reversing
changes made to 498c997ad7.

* 修改物料放下时的方法,如果选择

修改物料放下时的方法,
如果选择drop_trash,则删除物料显示
如果选择drop,则让其解除连接

* unilab添加moveit启动

1,整合所有moveit节点到一个move_group中,并整合所有的controller依次激活
2,添加pymoveit2的节点,使用json可直接启动
3,修改机械臂规划方式,添加约束,让冗余关节不会进行过多移动

* 修改物体attach时,多次赋值当前时间导致卡顿问题,

* Revert "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 56d45b94f5.

* Reapply "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 07d9db20c3.

* 添加缺少物料:"plate_well_G12",

* add

* fix tip resource data

* liquid states

* change to debug level

* Revert "change to debug level"

This reverts commit 5d9953c3e5.

* Reapply "change to debug level"

This reverts commit 2487bb6ffc.

* fix tip resource data

* add full device

* add moveit yaml

* 修复moveit
增加post_init阶段,给予ros_node反向

* remove necessary node

* fix moveit action client

* remove necessary imports

* Update moveit_interface.py

* fix handler_key uppercase

* json add liquids

* fix setup

* add

* change to "sources" and "targets" for lh

* bump version

* remove parent's parent link

* change arm's name

* change name

* fix ik error

* 修改moveit_interface,并在mqtt上报时发送一个时间戳

---------

Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>
Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
2025-07-08 15:49:35 +08:00
Xuwznln
ce8667f937 更新中析仪器,以及启动示例 2025-07-06 18:39:40 +08:00
41 changed files with 5197 additions and 4865 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
configs/
temp/
## Python
# Byte-compiled / optimized / DLL files

217
test/experiments/prcxi.json Normal file
View File

@@ -0,0 +1,217 @@
{
"nodes": [
{
"id": "PRCXI",
"name": "PRCXI",
"parent": null,
"type": "device",
"class": "liquid_handler.prcxi",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"deck": {
"_resource_child_name": "deck",
"_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck"
},
"host": "192.168.3.9",
"port": 9999,
"timeout": 10.0,
"setup": false,
"debug": true
},
"data": {},
"children": [
"deck"
]
},
{
"id": "deck",
"name": "deck",
"sample_id": null,
"children": [
"rackT1",
"plateT2",
"plateT3",
"rackT4",
"plateT5",
"plateT6"
],
"parent": "PRCXI",
"type": "device",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Deck"
},
"data": {}
},
{
"id": "rackT1",
"name": "rackT1",
"sample_id": null,
"children": [],
"parent": "deck",
"type": "device",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 120.98,
"size_y": 82.12,
"size_z": 50.3
},
"data": {
"Material": {
"uuid": "80652665f6a54402b2408d50b40398df",
"Code": "ZX-001-1000",
"Name": "1000μL Tip头",
"SummaryName": "1000μL Tip头",
"PipetteHeight": 100,
"materialEnum": 1
}
}
},
{
"id": "plateT2",
"name": "plateT2",
"sample_id": null,
"children": [],
"parent": "deck",
"type": "device",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 120.98,
"size_y": 82.12,
"size_z": 50.3
},
"data": {
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f"
}
}
},
{
"id": "plateT3",
"name": "plateT3",
"sample_id": null,
"children": [],
"parent": "deck",
"type": "device",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 120.98,
"size_y": 82.12,
"size_z": 50.3
},
"data": {
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f"
}
}
},
{
"id": "rackT4",
"name": "rackT4",
"sample_id": null,
"children": [],
"parent": "deck",
"type": "device",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 120.98,
"size_y": 82.12,
"size_z": 50.3
},
"data": {
"Material": {
"uuid": "80652665f6a54402b2408d50b40398df",
"Code": "ZX-001-1000",
"Name": "1000μL Tip头",
"SummaryName": "1000μL Tip头",
"PipetteHeight": 100,
"materialEnum": 1
}
}
},
{
"id": "plateT5",
"name": "plateT5",
"sample_id": null,
"children": [],
"parent": "deck",
"type": "device",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 120.98,
"size_y": 82.12,
"size_z": 50.3
},
"data": {
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f"
}
}
},
{
"id": "plateT6",
"name": "plateT6",
"sample_id": null,
"children": [],
"parent": "deck",
"type": "device",
"class": "",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "PRCXI9300Container",
"size_x": 120.98,
"size_y": 82.12,
"size_z": 50.3
},
"data": {
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f"
}
}
}
],
"links": []
}

View File

@@ -1,8 +1,10 @@
import json
import traceback
import uuid
from unilabos.app.model import JobAddReq, JobData
from unilabos.ros.nodes.presets.host_node import HostNode
from unilabos.utils.type_check import serialize_result_info
def get_resources() -> tuple:
@@ -33,5 +35,10 @@ def job_add(req: JobAddReq) -> JobData:
if "command" in action_args:
action_args = action_args["command"]
# print(f"job_add:{req.device_id} {action_name} {action_kwargs}")
HostNode.get_instance().send_goal(req.device_id, action_type=action_type, action_name=action_name, action_kwargs=action_args, goal_uuid=req.job_id, server_info=req.server_info)
try:
HostNode.get_instance().send_goal(req.device_id, action_type=action_type, action_name=action_name, action_kwargs=action_args, goal_uuid=req.job_id, server_info=req.server_info)
except Exception as e:
for bridge in HostNode.get_instance().bridges:
if hasattr(bridge, "publish_job_status"):
bridge.publish_job_status({}, req.job_id, "failed", serialize_result_info(traceback.format_exc(), False, {}))
return JobData(jobId=req.job_id)

View File

@@ -163,7 +163,7 @@ class MQTTClient:
# status = device_status.get(device_id, {})
if self.mqtt_disable:
return
status = {"data": device_status.get(device_id, {}), "device_id": device_id}
status = {"data": device_status.get(device_id, {}), "device_id": device_id, "timestamp": time.time()}
address = f"labs/{MQConfig.lab_id}/devices/"
self.client.publish(address, json.dumps(status), qos=2)
logger.debug(f"Device status published: address: {address}, {status}")

View File

@@ -22,18 +22,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
debug_print(f"📏 体积输入为数值: {volume_input}")
return float(volume_input)
if not volume_input or not str(volume_input).strip():
debug_print(f"⚠️ 体积输入为空返回0.0mL")
return 0.0
volume_str = str(volume_input).lower().strip()
debug_print(f"解析体积输入: '{volume_str}'")
debug_print(f"🔍 解析体积输入: '{volume_str}'")
# 处理未知体积
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_volume = 10.0 # 默认10mL
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL 🎯")
return default_volume
# 移除空格并提取数字和单位
@@ -43,7 +45,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
debug_print(f"⚠️ 无法解析体积: '{volume_str}'使用默认值10mL")
debug_print(f" 无法解析体积: '{volume_str}'使用默认值10mL")
return 10.0
value = float(match.group(1))
@@ -52,12 +54,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
debug_print(f"🔄 体积转换: {value}L → {volume}mL")
elif unit in ['μl', 'ul', 'microliter']:
volume = value / 1000.0 # μL -> mL
debug_print(f"🔄 体积转换: {value}μL → {volume}mL")
else: # ml, milliliter 或默认
volume = value # 已经是mL
debug_print(f"✅ 体积已为mL: {volume}mL")
debug_print(f"体积转换: {value}{unit}{volume}mL")
return volume
def parse_mass_input(mass_input: Union[str, float]) -> float:
@@ -71,13 +75,15 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
float: 质量(克)
"""
if isinstance(mass_input, (int, float)):
debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
return float(mass_input)
if not mass_input or not str(mass_input).strip():
debug_print(f"⚠️ 质量输入为空返回0.0g")
return 0.0
mass_str = str(mass_input).lower().strip()
debug_print(f"解析质量输入: '{mass_str}'")
debug_print(f"🔍 解析质量输入: '{mass_str}'")
# 移除空格并提取数字和单位
mass_clean = re.sub(r'\s+', '', mass_str)
@@ -86,7 +92,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
if not match:
debug_print(f"⚠️ 无法解析质量: '{mass_str}'返回0.0g")
debug_print(f" 无法解析质量: '{mass_str}'返回0.0g")
return 0.0
value = float(match.group(1))
@@ -95,12 +101,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
# 转换为克
if unit in ['mg', 'milligram']:
mass = value / 1000.0 # mg -> g
debug_print(f"🔄 质量转换: {value}mg → {mass}g")
elif unit in ['kg', 'kilogram']:
mass = value * 1000.0 # kg -> g
debug_print(f"🔄 质量转换: {value}kg → {mass}g")
else: # g, gram 或默认
mass = value # 已经是g
debug_print(f"✅ 质量已为g: {mass}g")
debug_print(f"质量转换: {value}{unit}{mass}g")
return mass
def parse_time_input(time_input: Union[str, float]) -> float:
@@ -114,18 +122,20 @@ def parse_time_input(time_input: Union[str, float]) -> float:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
debug_print(f"⏱️ 时间输入为数值: {time_input}")
return float(time_input)
if not time_input or not str(time_input).strip():
debug_print(f"⚠️ 时间输入为空返回0秒")
return 0.0
time_str = str(time_input).lower().strip()
debug_print(f"解析时间输入: '{time_str}'")
debug_print(f"🔍 解析时间输入: '{time_str}'")
# 处理未知时间
if time_str in ['?', 'unknown', 'tbd']:
default_time = 60.0 # 默认1分钟
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
debug_print(f"检测到未知时间,使用默认值: {default_time}s (1分钟) ⏰")
return default_time
# 移除空格并提取数字和单位
@@ -135,7 +145,7 @@ def parse_time_input(time_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
if not match:
debug_print(f"⚠️ 无法解析时间: '{time_str}'返回0s")
debug_print(f" 无法解析时间: '{time_str}'返回0s")
return 0.0
value = float(match.group(1))
@@ -144,21 +154,25 @@ def parse_time_input(time_input: Union[str, float]) -> float:
# 转换为秒
if unit in ['min', 'minute']:
time_sec = value * 60.0 # min -> s
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}")
elif unit in ['h', 'hr', 'hour']:
time_sec = value * 3600.0 # h -> s
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}")
elif unit in ['d', 'day']:
time_sec = value * 86400.0 # d -> s
debug_print(f"🔄 时间转换: {value}天 → {time_sec}")
else: # s, sec, second 或默认
time_sec = value # 已经是s
debug_print(f"✅ 时间已为秒: {time_sec}")
debug_print(f"时间转换: {value}{unit}{time_sec}s")
return time_sec
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
"""增强版试剂容器查找,支持固体和液体"""
debug_print(f"查找试剂 '{reagent}' 的容器...")
debug_print(f"🔍 开始查找试剂 '{reagent}' 的容器...")
# 🔧 方法1直接搜索 data.reagent_name 和 config.reagent
debug_print(f"📋 方法1: 搜索reagent字段...")
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
@@ -171,16 +185,17 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
# 精确匹配
if reagent_name == reagent.lower() or config_reagent == reagent.lower():
debug_print(f"✅ 通过reagent字段到容器: {node}")
debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
return node
# 模糊匹配
if (reagent.lower() in reagent_name and reagent_name) or \
(reagent.lower() in config_reagent and config_reagent):
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
return node
# 🔧 方法2常见的容器命名规则
debug_print(f"📋 方法2: 使用命名规则查找...")
reagent_clean = reagent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
reagent_clean,
@@ -197,20 +212,23 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
f"reagent_bottle_3"
]
debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
if node_type == 'container':
debug_print(f"✅ 通过命名规则找到容器: {name}")
debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
return name
# 🔧 方法3节点名称模糊匹配
debug_print(f"📋 方法3: 节点名称模糊匹配...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if node_data.get('type') == 'container':
# 检查节点名称是否包含试剂名称
if reagent_clean in node_id.lower():
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id}")
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
return node_id
# 检查液体类型匹配
@@ -220,51 +238,77 @@ def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == reagent.lower():
debug_print(f"✅ 通过液体类型匹配到容器: {node_id}")
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
return node_id
# 🔧 方法4使用第一个试剂瓶作为备选
debug_print(f"📋 方法4: 查找备选试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id}")
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
return node_id
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
stirrer_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'stirrer' in node_class:
stirrer_nodes.append(node)
debug_print(f"📋 发现搅拌器: {node}")
debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"找到连接的搅拌器: {stirrer}")
debug_print(f"找到连接的搅拌器: {stirrer} 🔗")
return stirrer
# 返回第一个搅拌器
if stirrer_nodes:
debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}")
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
return stirrer_nodes[0]
debug_print(f"❌ 未找到任何搅拌器")
return ""
def find_solid_dispenser(G: nx.DiGraph) -> str:
"""查找固体加样器"""
debug_print(f"🔍 查找固体加样器...")
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
debug_print(f"找到固体加样器: {node}")
debug_print(f"找到固体加样器: {node} 🥄")
return node
debug_print("⚠️ 未找到固体加样器")
debug_print(f" 未找到固体加样器")
return ""
# 🆕 创建进度日志动作
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
"""创建一个动作日志"""
full_message = f"{emoji} {message}"
debug_print(full_message)
logger.info(full_message)
print(f"[ACTION] {full_message}", flush=True)
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": full_message
}
}
def generate_add_protocol(
G: nx.DiGraph,
vessel: str,
@@ -301,51 +345,58 @@ def generate_add_protocol(
"""
debug_print("=" * 60)
debug_print("开始生成添加试剂协议")
debug_print(f"原始参数:")
debug_print(f" - vessel: '{vessel}'")
debug_print(f" - reagent: '{reagent}'")
debug_print(f" - volume: {volume} (类型: {type(volume)})")
debug_print(f" - mass: {mass} (类型: {type(mass)})")
debug_print(f" - time: {time} (类型: {type(time)})")
debug_print(f" - mol: '{mol}'")
debug_print(f" - event: '{event}'")
debug_print(f" - rate_spec: '{rate_spec}'")
debug_print("🚀 开始生成添加试剂协议")
debug_print(f"📋 原始参数:")
debug_print(f" 🥼 vessel: '{vessel}'")
debug_print(f" 🧪 reagent: '{reagent}'")
debug_print(f" 📏 volume: {volume} (类型: {type(volume)})")
debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})")
debug_print(f" ⏱️ time: {time} (类型: {type(time)})")
debug_print(f" 🧬 mol: '{mol}'")
debug_print(f" 🎯 event: '{event}'")
debug_print(f" rate_spec: '{rate_spec}'")
debug_print(f" 🌪️ stir: {stir}")
debug_print(f" 🔄 stir_speed: {stir_speed} rpm")
debug_print("=" * 60)
action_sequence = []
# === 参数验证 ===
debug_print("步骤1: 参数验证...")
debug_print("🔍 步骤1: 参数验证...")
action_sequence.append(create_action_log(f"开始添加试剂 '{reagent}' 到容器 '{vessel}'", "🎬"))
if not vessel:
debug_print("❌ vessel 参数不能为空")
raise ValueError("vessel 参数不能为空")
if not reagent:
debug_print("❌ reagent 参数不能为空")
raise ValueError("reagent 参数不能为空")
if vessel not in G.nodes():
debug_print(f"❌ 容器 '{vessel}' 不存在于系统中")
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
debug_print("✅ 基本参数验证通过")
# === 🔧 关键修复:参数解析 ===
debug_print("步骤2: 参数解析...")
debug_print("🔍 步骤2: 参数解析...")
action_sequence.append(create_action_log("正在解析添加参数...", "🔍"))
# 解析各种参数为数值
final_volume = parse_volume_input(volume)
final_mass = parse_mass_input(mass)
final_time = parse_time_input(time)
debug_print(f"解析结果:")
debug_print(f" - 体积: {final_volume}mL")
debug_print(f" - 质量: {final_mass}g")
debug_print(f" - 时间: {final_time}s")
debug_print(f" - 摩尔: '{mol}'")
debug_print(f" - 事件: '{event}'")
debug_print(f" - 速率: '{rate_spec}'")
debug_print(f"📊 解析结果:")
debug_print(f" 📏 体积: {final_volume}mL")
debug_print(f" ⚖️ 质量: {final_mass}g")
debug_print(f" ⏱️ 时间: {final_time}s")
debug_print(f" 🧬 摩尔: '{mol}'")
debug_print(f" 🎯 事件: '{event}'")
debug_print(f" 速率: '{rate_spec}'")
# === 判断添加类型 ===
debug_print("步骤3: 判断添加类型...")
debug_print("🔍 步骤3: 判断添加类型...")
# 🔧 修复:现在使用解析后的数值进行比较
is_solid = (final_mass > 0 or (mol and mol.strip() != ""))
@@ -357,22 +408,34 @@ def generate_add_protocol(
final_volume = 10.0
debug_print("⚠️ 未指定体积或质量默认为10mL液体")
debug_print(f"添加类型: {'固体' if is_solid else '液体'}")
add_type = "固体" if is_solid else "液体"
add_emoji = "🧂" if is_solid else "💧"
debug_print(f"📋 添加类型: {add_type} {add_emoji}")
action_sequence.append(create_action_log(f"确定添加类型: {add_type} {add_emoji}", "📋"))
# === 执行添加流程 ===
debug_print("步骤4: 执行添加流程...")
debug_print("🔍 步骤4: 执行添加流程...")
try:
if is_solid:
# === 固体添加路径 ===
debug_print(f"使用固体添加路径")
debug_print(f"🧂 使用固体添加路径")
action_sequence.append(create_action_log("开始固体试剂添加流程", "🧂"))
solid_dispenser = find_solid_dispenser(G)
if solid_dispenser:
action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄"))
# 启动搅拌
if stir:
debug_print("🌪️ 准备启动搅拌...")
action_sequence.append(create_action_log("准备启动搅拌器", "🌪️"))
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄"))
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -383,6 +446,7 @@ def generate_add_protocol(
}
})
# 等待搅拌稳定
action_sequence.append(create_action_log("等待搅拌稳定...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 3}
@@ -399,19 +463,27 @@ def generate_add_protocol(
if final_mass > 0:
add_kwargs["mass"] = str(final_mass)
action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️"))
if mol and mol.strip():
add_kwargs["mol"] = mol
action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬"))
if equiv and equiv.strip():
add_kwargs["equiv"] = equiv
action_sequence.append(create_action_log(f"当量: {equiv}", "🔢"))
action_sequence.append(create_action_log("开始固体加样操作", "🥄"))
action_sequence.append({
"device_id": solid_dispenser,
"action_name": "add_solid",
"action_kwargs": add_kwargs
})
action_sequence.append(create_action_log("固体加样完成", ""))
# 添加后等待
if final_time > 0:
wait_minutes = final_time / 60
action_sequence.append(create_action_log(f"等待反应进行 ({wait_minutes:.1f}分钟)", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": final_time}
@@ -419,19 +491,28 @@ def generate_add_protocol(
debug_print(f"✅ 固体添加完成")
else:
debug_print("⚠️ 未找到固体加样器,跳过固体添加")
debug_print(" 未找到固体加样器,跳过固体添加")
action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", ""))
else:
# === 液体添加路径 ===
debug_print(f"使用液体添加路径")
debug_print(f"💧 使用液体添加路径")
action_sequence.append(create_action_log("开始液体试剂添加流程", "💧"))
# 查找试剂容器
action_sequence.append(create_action_log("正在查找试剂容器...", "🔍"))
reagent_vessel = find_reagent_vessel(G, reagent)
action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪"))
# 启动搅拌
if stir:
debug_print("🌪️ 准备启动搅拌...")
action_sequence.append(create_action_log("准备启动搅拌器", "🌪️"))
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed} rpm)", "🔄"))
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -442,6 +523,7 @@ def generate_add_protocol(
}
})
# 等待搅拌稳定
action_sequence.append(create_action_log("等待搅拌稳定...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
@@ -451,18 +533,23 @@ def generate_add_protocol(
if final_time > 0:
flowrate = final_volume / final_time * 60 # mL/min
transfer_flowrate = flowrate
debug_print(f"⚡ 根据时间计算流速: {flowrate:.2f} mL/min")
else:
if rate_spec == "dropwise":
flowrate = 0.5 # 滴加,很慢
transfer_flowrate = 0.2
debug_print(f"💧 滴加模式,流速: {flowrate} mL/min")
elif viscous:
flowrate = 1.0 # 粘性液体
transfer_flowrate = 0.3
debug_print(f"🍯 粘性液体,流速: {flowrate} mL/min")
else:
flowrate = 2.5 # 正常流速
transfer_flowrate = 0.5
debug_print(f"⚡ 正常流速: {flowrate} mL/min")
debug_print(f"流速设置: {flowrate} mL/min")
action_sequence.append(create_action_log(f"设置流速: {flowrate:.2f} mL/min", ""))
action_sequence.append(create_action_log(f"开始转移 {final_volume}mL 液体", "🚰"))
# 调用pump protocol
pump_actions = generate_pump_protocol_with_rinsing(
@@ -486,9 +573,11 @@ def generate_add_protocol(
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 液体转移完成,添加了 {len(pump_actions)} 个动作")
action_sequence.append(create_action_log(f"液体转移完成 ({len(pump_actions)} 个操作)", ""))
except Exception as e:
debug_print(f"⚠️ 试剂添加失败: {str(e)}")
debug_print(f" 试剂添加失败: {str(e)}")
action_sequence.append(create_action_log(f"试剂添加失败: {str(e)}", ""))
# 添加错误日志
action_sequence.append({
"device_id": "system",
@@ -500,19 +589,28 @@ def generate_add_protocol(
# === 最终结果 ===
debug_print("=" * 60)
debug_print(f" 添加试剂协议生成完成")
debug_print(f"🎉 添加试剂协议生成完成")
debug_print(f"📊 总动作数: {len(action_sequence)}")
debug_print(f"📋 处理总结:")
debug_print(f" - 试剂: {reagent}")
debug_print(f" - 添加类型: {'固体' if is_solid else '液体'}")
debug_print(f" - 目标容器: {vessel}")
debug_print(f" 🧪 试剂: {reagent}")
debug_print(f" {add_emoji} 添加类型: {add_type}")
debug_print(f" 🥼 目标容器: {vessel}")
if is_liquid:
debug_print(f" - 体积: {final_volume}mL")
debug_print(f" 📏 体积: {final_volume}mL")
if is_solid:
debug_print(f" - 质量: {final_mass}g")
debug_print(f" - 摩尔: {mol}")
debug_print(f" ⚖️ 质量: {final_mass}g")
debug_print(f" 🧬 摩尔: {mol}")
debug_print("=" * 60)
# 添加完成日志
summary_msg = f"试剂添加协议完成: {reagent}{vessel}"
if is_liquid:
summary_msg += f" ({final_volume}mL)"
if is_solid:
summary_msg += f" ({final_mass}g)"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
# === 便捷函数 ===
@@ -520,6 +618,7 @@ def generate_add_protocol(
def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float],
time: Union[str, float] = 0.0, rate_spec: str = "") -> List[Dict[str, Any]]:
"""添加指定体积的液体试剂"""
debug_print(f"💧 快速添加液体: {reagent} ({volume}) → {vessel}")
return generate_add_protocol(
G, vessel, reagent,
volume=volume,
@@ -530,6 +629,7 @@ def add_liquid_volume(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[st
def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
event: str = "") -> List[Dict[str, Any]]:
"""添加指定质量的固体试剂"""
debug_print(f"🧂 快速添加固体: {reagent} ({mass}) → {vessel}")
return generate_add_protocol(
G, vessel, reagent,
mass=mass,
@@ -539,6 +639,7 @@ def add_solid_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, fl
def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
event: str = "") -> List[Dict[str, Any]]:
"""按摩尔数添加固体试剂"""
debug_print(f"🧬 按摩尔数添加固体: {reagent} ({mol}) → {vessel}")
return generate_add_protocol(
G, vessel, reagent,
mol=mol,
@@ -548,6 +649,7 @@ def add_solid_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[str, float],
time: Union[str, float] = "20 min", event: str = "") -> List[Dict[str, Any]]:
"""滴加液体试剂"""
debug_print(f"💧 滴加液体: {reagent} ({volume}) → {vessel} (用时: {time})")
return generate_add_protocol(
G, vessel, reagent,
volume=volume,
@@ -559,6 +661,7 @@ def add_dropwise_liquid(G: nx.DiGraph, vessel: str, reagent: str, volume: Union[
def add_portionwise_solid(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
time: Union[str, float] = "1 h", event: str = "") -> List[Dict[str, Any]]:
"""分批添加固体试剂"""
debug_print(f"🧂 分批添加固体: {reagent} ({mass}) → {vessel} (用时: {time})")
return generate_add_protocol(
G, vessel, reagent,
mass=mass,
@@ -573,22 +676,25 @@ def test_add_protocol():
print("=== ADD PROTOCOL 增强版测试 ===")
# 测试体积解析
debug_print("🧪 测试体积解析...")
volumes = ["2.7 mL", "2.67 mL", "?", 10.0, "1 L", "500 μL"]
for vol in volumes:
result = parse_volume_input(vol)
print(f"体积解析: {vol}{result}mL")
print(f"📏 体积解析: {vol}{result}mL")
# 测试质量解析
debug_print("⚖️ 测试质量解析...")
masses = ["19.3 g", "4.5 g", 2.5, "500 mg", "1 kg"]
for mass in masses:
result = parse_mass_input(mass)
print(f"质量解析: {mass}{result}g")
print(f"⚖️ 质量解析: {mass}{result}g")
# 测试时间解析
debug_print("⏱️ 测试时间解析...")
times = ["1 h", "20 min", "30 s", 60.0, "?"]
for time in times:
result = parse_time_input(time)
print(f"时间解析: {time}{result}s")
print(f"⏱️ 时间解析: {time}{result}s")
print("✅ 测试完成")

View File

@@ -1,7 +1,30 @@
import networkx as nx
import logging
from typing import List, Dict, Any
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
def debug_print(message):
"""调试输出"""
print(f"[ADJUST_PH] {message}", flush=True)
logger.info(f"[ADJUST_PH] {message}")
# 🆕 创建进度日志动作
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
"""创建一个动作日志"""
full_message = f"{emoji} {message}"
debug_print(full_message)
logger.info(full_message)
print(f"[ACTION] {full_message}", flush=True)
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": full_message
}
}
def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
"""
@@ -14,7 +37,7 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
Returns:
str: 试剂容器ID
"""
print(f"ADJUST_PH: 正在查找试剂 '{reagent}' 的容器...")
debug_print(f"🔍 正在查找试剂 '{reagent}' 的容器...")
# 常见酸碱试剂的别名映射
reagent_aliases = {
@@ -29,11 +52,16 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
# 构建搜索名称列表
search_names = [reagent.lower()]
debug_print(f"📋 基础搜索名称: {reagent.lower()}")
# 添加别名
for base_name, aliases in reagent_aliases.items():
if reagent.lower() in base_name.lower() or base_name.lower() in reagent.lower():
search_names.extend([alias.lower() for alias in aliases])
debug_print(f"🔗 添加别名: {aliases}")
break
debug_print(f"📝 完整搜索列表: {search_names}")
# 构建可能的容器名称
possible_names = []
@@ -49,13 +77,17 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
name_clean
])
debug_print(f"🎯 可能的容器名称 (前5个): {possible_names[:5]}... (共{len(possible_names)}个)")
# 第一步:通过容器名称匹配
debug_print(f"📋 方法1: 精确名称匹配...")
for vessel_name in possible_names:
if vessel_name in G.nodes():
print(f"ADJUST_PH: 通过名称匹配找到容器: {vessel_name}")
debug_print(f" 通过名称匹配找到容器: {vessel_name} 🎯")
return vessel_name
# 第二步:通过模糊匹配
debug_print(f"📋 方法2: 模糊名称匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
node_name = G.nodes[node_id].get('name', '').lower()
@@ -63,10 +95,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
# 检查是否包含任何搜索名称
for search_name in search_names:
if search_name in node_id.lower() or search_name in node_name:
print(f"ADJUST_PH: 通过模糊匹配找到容器: {node_id}")
debug_print(f" 通过模糊匹配找到容器: {node_id} 🔍")
return node_id
# 第三步:通过液体类型匹配
debug_print(f"📋 方法3: 液体类型匹配...")
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
@@ -79,10 +112,11 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
for search_name in search_names:
if search_name in liquid_type or search_name in reagent_name:
print(f"ADJUST_PH: 通过液体类型匹配找到容器: {node_id}")
debug_print(f" 通过液体类型匹配找到容器: {node_id} 💧")
return node_id
# 列出可用容器帮助调试
debug_print(f"📊 列出可用容器帮助调试...")
available_containers = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
@@ -98,67 +132,92 @@ def find_acid_base_vessel(G: nx.DiGraph, reagent: str) -> str:
'reagent_name': vessel_data.get('reagent_name', '')
})
print(f"ADJUST_PH: 可用容器列表:")
debug_print(f"📋 可用容器列表:")
for container in available_containers:
print(f" - {container['id']}: {container['name']}")
print(f" 液体: {container['liquids']}")
print(f" 试剂: {container['reagent_name']}")
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names}")
debug_print(f" - 🧪 {container['id']}: {container['name']}")
debug_print(f" 💧 液体: {container['liquids']}")
debug_print(f" 🏷️ 试剂: {container['reagent_name']}")
debug_print(f"❌ 所有匹配方法都失败了")
raise ValueError(f"找不到试剂 '{reagent}' 对应的容器。尝试了: {possible_names[:10]}...")
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找与容器相连的搅拌器"""
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
stirrer_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
debug_print(f"📊 发现 {len(stirrer_nodes)} 个搅拌器: {stirrer_nodes}")
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"✅ 找到连接的搅拌器: {stirrer} 🔗")
return stirrer
return stirrer_nodes[0] if stirrer_nodes else None
if stirrer_nodes:
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
return stirrer_nodes[0]
debug_print(f"❌ 未找到任何搅拌器")
return None
def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float: # 改为 target_ph_value
def calculate_reagent_volume(target_ph_value: float, reagent: str, vessel_volume: float = 100.0) -> float:
"""
估算需要的试剂体积来调节pH
Args:
target_ph_value: 目标pH值 # 改为 target_ph_value
target_ph_value: 目标pH值
reagent: 试剂名称
vessel_volume: 容器体积 (mL)
Returns:
float: 估算的试剂体积 (mL)
"""
debug_print(f"🧮 计算试剂体积...")
debug_print(f" 📍 目标pH: {target_ph_value}")
debug_print(f" 🧪 试剂: {reagent}")
debug_print(f" 📏 容器体积: {vessel_volume}mL")
# 简化的pH调节体积估算实际应用中需要更精确的计算
if "acid" in reagent.lower() or "hcl" in reagent.lower():
debug_print(f"🍋 检测到酸性试剂")
# 酸性试剂pH越低需要的体积越大
if target_ph_value < 3: # 改为 target_ph_value
return vessel_volume * 0.05 # 5%
elif target_ph_value < 5: # 改为 target_ph_value
return vessel_volume * 0.02 # 2%
if target_ph_value < 3:
volume = vessel_volume * 0.05 # 5%
debug_print(f" 💪 强酸性 (pH<3): 使用 5% 体积")
elif target_ph_value < 5:
volume = vessel_volume * 0.02 # 2%
debug_print(f" 🔸 中酸性 (pH<5): 使用 2% 体积")
else:
return vessel_volume * 0.01 # 1%
volume = vessel_volume * 0.01 # 1%
debug_print(f" 🔹 弱酸性 (pH≥5): 使用 1% 体积")
elif "hydroxide" in reagent.lower() or "naoh" in reagent.lower():
debug_print(f"🧂 检测到碱性试剂")
# 碱性试剂pH越高需要的体积越大
if target_ph_value > 11: # 改为 target_ph_value
return vessel_volume * 0.05 # 5%
elif target_ph_value > 9: # 改为 target_ph_value
return vessel_volume * 0.02 # 2%
if target_ph_value > 11:
volume = vessel_volume * 0.05 # 5%
debug_print(f" 💪 强碱性 (pH>11): 使用 5% 体积")
elif target_ph_value > 9:
volume = vessel_volume * 0.02 # 2%
debug_print(f" 🔸 中碱性 (pH>9): 使用 2% 体积")
else:
return vessel_volume * 0.01 # 1%
volume = vessel_volume * 0.01 # 1%
debug_print(f" 🔹 弱碱性 (pH≤9): 使用 1% 体积")
else:
# 未知试剂,使用默认值
return vessel_volume * 0.01
volume = vessel_volume * 0.01
debug_print(f"❓ 未知试剂类型,使用默认 1% 体积")
debug_print(f"📊 计算结果: {volume:.2f}mL")
return volume
def generate_adjust_ph_protocol(
G: nx.DiGraph,
vessel: str,
ph_value: float, # 改为 ph_value
ph_value: float,
reagent: str,
**kwargs
) -> List[Dict[str, Any]]:
@@ -168,13 +227,23 @@ def generate_adjust_ph_protocol(
Args:
G: 有向图,节点为容器和设备
vessel: 目标容器需要调节pH的容器
ph_value: 目标pH值从XDL传入 # 改为 ph_value
ph_value: 目标pH值从XDL传入
reagent: 酸碱试剂名称从XDL传入
**kwargs: 其他可选参数,使用默认值
Returns:
List[Dict[str, Any]]: 动作序列
"""
debug_print("=" * 60)
debug_print("🧪 开始生成pH调节协议")
debug_print(f"📋 原始参数:")
debug_print(f" 🥼 vessel: '{vessel}'")
debug_print(f" 📊 ph_value: {ph_value}")
debug_print(f" 🧪 reagent: '{reagent}'")
debug_print(f" 📦 kwargs: {kwargs}")
debug_print("=" * 60)
action_sequence = []
# 从kwargs中获取可选参数如果没有则使用默认值
@@ -184,48 +253,84 @@ def generate_adjust_ph_protocol(
stir_time = kwargs.get('stir_time', 60.0) # 默认搅拌时间
settling_time = kwargs.get('settling_time', 30.0) # 默认平衡时间
print(f"ADJUST_PH: 开始生成pH调节协议")
print(f" - 目标容器: {vessel}")
print(f" - 目标pH: {ph_value}") # 改为 ph_value
print(f" - 试剂: {reagent}")
print(f" - 使用默认参数: 体积=自动估算, 搅拌=True, 搅拌速度=300RPM")
debug_print(f"🔧 处理后的参数:")
debug_print(f" 📏 volume: {volume}mL (0.0表示自动估算)")
debug_print(f" 🌪️ stir: {stir}")
debug_print(f" 🔄 stir_speed: {stir_speed}rpm")
debug_print(f" ⏱️ stir_time: {stir_time}s")
debug_print(f" ⏳ settling_time: {settling_time}s")
# 开始处理
action_sequence.append(create_action_log(f"开始调节pH至 {ph_value}", "🧪"))
action_sequence.append(create_action_log(f"目标容器: {vessel}", "🥼"))
action_sequence.append(create_action_log(f"使用试剂: {reagent}", "⚗️"))
# 1. 验证目标容器存在
debug_print(f"🔍 步骤1: 验证目标容器...")
if vessel not in G.nodes():
debug_print(f"❌ 目标容器 '{vessel}' 不存在于系统中")
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
debug_print(f"✅ 目标容器验证通过")
action_sequence.append(create_action_log("目标容器验证通过", ""))
# 2. 查找酸碱试剂容器
debug_print(f"🔍 步骤2: 查找试剂容器...")
action_sequence.append(create_action_log("正在查找试剂容器...", "🔍"))
try:
reagent_vessel = find_acid_base_vessel(G, reagent)
print(f"ADJUST_PH: 找到试剂容器: {reagent_vessel}")
debug_print(f" 找到试剂容器: {reagent_vessel}")
action_sequence.append(create_action_log(f"找到试剂容器: {reagent_vessel}", "🧪"))
except ValueError as e:
debug_print(f"❌ 无法找到试剂容器: {str(e)}")
action_sequence.append(create_action_log(f"试剂容器查找失败: {str(e)}", ""))
raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}")
# 3. 如果未指定体积,自动估算
# 3. 体积估算
debug_print(f"🔍 步骤3: 体积处理...")
if volume <= 0:
action_sequence.append(create_action_log("开始自动估算试剂体积", "🧮"))
# 获取目标容器的体积信息
vessel_data = G.nodes[vessel].get('data', {})
vessel_volume = vessel_data.get('max_volume', 100.0) # 默认100mL
debug_print(f"📏 容器最大体积: {vessel_volume}mL")
estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume) # 改为 ph_value
estimated_volume = calculate_reagent_volume(ph_value, reagent, vessel_volume)
volume = estimated_volume
print(f"ADJUST_PH: 自动估算试剂体积: {volume:.2f} mL")
debug_print(f" 自动估算试剂体积: {volume:.2f} mL")
action_sequence.append(create_action_log(f"估算试剂体积: {volume:.2f}mL", "📊"))
else:
debug_print(f"📏 使用指定体积: {volume}mL")
action_sequence.append(create_action_log(f"使用指定体积: {volume}mL", "📏"))
# 4. 验证路径存在
debug_print(f"🔍 步骤4: 路径验证...")
action_sequence.append(create_action_log("验证转移路径...", "🛤️"))
try:
path = nx.shortest_path(G, source=reagent_vessel, target=vessel)
print(f"ADJUST_PH: 找到路径: {''.join(path)}")
debug_print(f" 找到路径: {''.join(path)}")
action_sequence.append(create_action_log(f"找到转移路径: {''.join(path)}", "🛤️"))
except nx.NetworkXNoPath:
debug_print(f"❌ 无法找到转移路径")
action_sequence.append(create_action_log("转移路径不存在", ""))
raise ValueError(f"从试剂容器 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径")
# 5. 先启动搅拌(如果需要)
# 5. 搅拌器设置
debug_print(f"🔍 步骤5: 搅拌器设置...")
stirrer_id = None
if stir:
action_sequence.append(create_action_log("准备启动搅拌器", "🌪️"))
try:
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
print(f"ADJUST_PH: 找到搅拌器 {stirrer_id},启动搅拌")
debug_print(f" 找到搅拌器 {stirrer_id},启动搅拌")
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🔄"))
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -237,23 +342,34 @@ def generate_adjust_ph_protocol(
})
# 等待搅拌稳定
action_sequence.append(create_action_log("等待搅拌稳定...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
else:
print(f"ADJUST_PH: 警告 - 未找到搅拌器,继续执行")
debug_print(f"⚠️ 未找到搅拌器,继续执行")
action_sequence.append(create_action_log("未找到搅拌器,跳过搅拌", "⚠️"))
except Exception as e:
print(f"ADJUST_PH: 搅拌器配置出错: {str(e)}")
debug_print(f" 搅拌器配置出错: {str(e)}")
action_sequence.append(create_action_log(f"搅拌器配置失败: {str(e)}", ""))
else:
debug_print(f"📋 跳过搅拌设置")
action_sequence.append(create_action_log("跳过搅拌设置", "⏭️"))
# 6. 缓慢添加试剂 - 使用pump_protocol
print(f"ADJUST_PH: 开始添加试剂 {volume:.2f} mL")
# 6. 试剂添加
debug_print(f"🔍 步骤6: 试剂添加...")
action_sequence.append(create_action_log(f"开始添加试剂 {volume:.2f}mL", "🚰"))
# 计算添加时间pH调节需要缓慢添加
addition_time = max(30.0, volume * 2.0) # 至少30秒每mL需要2秒
debug_print(f"⏱️ 计算添加时间: {addition_time}s (缓慢注入)")
action_sequence.append(create_action_log(f"设置添加时间: {addition_time:.0f}s (缓慢注入)", "⏱️"))
try:
action_sequence.append(create_action_log("调用泵协议进行试剂转移", "🔄"))
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=reagent_vessel,
@@ -266,17 +382,24 @@ def generate_adjust_ph_protocol(
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=0.5 # 缓慢注入
flowrate=0.5, # 缓慢注入
transfer_flowrate=0.3
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 泵协议生成完成,添加了 {len(pump_actions)} 个动作")
action_sequence.append(create_action_log(f"试剂转移完成 ({len(pump_actions)} 个操作)", ""))
except Exception as e:
debug_print(f"❌ 生成泵协议时出错: {str(e)}")
action_sequence.append(create_action_log(f"泵协议生成失败: {str(e)}", ""))
raise ValueError(f"生成泵协议时出错: {str(e)}")
# 7. 持续搅拌以混合和平衡
# 7. 混合搅拌
if stir and stirrer_id:
print(f"ADJUST_PH: 持续搅拌 {stir_time} 秒以混合试剂")
debug_print(f"🔍 步骤7: 混合搅拌...")
action_sequence.append(create_action_log(f"开始混合搅拌 {stir_time:.0f}s", "🌀"))
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stir",
@@ -284,25 +407,47 @@ def generate_adjust_ph_protocol(
"stir_time": stir_time,
"stir_speed": stir_speed,
"settling_time": settling_time,
"purpose": f"pH调节: 混合试剂目标pH={ph_value}" # 改为 ph_value
"purpose": f"pH调节: 混合试剂目标pH={ph_value}"
}
})
# 8. 等待反应平衡
debug_print(f"✅ 混合搅拌设置完成")
else:
debug_print(f"⏭️ 跳过混合搅拌")
action_sequence.append(create_action_log("跳过混合搅拌", "⏭️"))
# 8. 等待平衡
debug_print(f"🔍 步骤8: 反应平衡...")
action_sequence.append(create_action_log(f"等待pH平衡 {settling_time:.0f}s", "⚖️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": settling_time,
"description": f"等待pH平衡到目标值 {ph_value}" # 改为 ph_value
"description": f"等待pH平衡到目标值 {ph_value}"
}
})
print(f"ADJUST_PH: 协议生成完成,共 {len(action_sequence)} 个动作")
print(f"ADJUST_PH: 预计总时间: {addition_time + stir_time + settling_time:.0f}")
# 9. 完成总结
total_time = addition_time + stir_time + settling_time
debug_print("=" * 60)
debug_print(f"🎉 pH调节协议生成完成")
debug_print(f"📊 协议统计:")
debug_print(f" 📋 总动作数: {len(action_sequence)}")
debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f}分钟)")
debug_print(f" 🧪 试剂: {reagent}")
debug_print(f" 📏 体积: {volume:.2f}mL")
debug_print(f" 📊 目标pH: {ph_value}")
debug_print(f" 🥼 目标容器: {vessel}")
debug_print("=" * 60)
# 添加完成日志
summary_msg = f"pH调节协议完成: {vessel} → pH {ph_value} (使用 {volume:.2f}mL {reagent})"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
def generate_adjust_ph_protocol_stepwise(
G: nx.DiGraph,
vessel: str,
@@ -317,7 +462,7 @@ def generate_adjust_ph_protocol_stepwise(
Args:
G: 网络图
vessel: 目标容器
pH: 目标pH值
ph_value: 目标pH值
reagent: 酸碱试剂
max_volume: 最大试剂体积
steps: 分步数量
@@ -325,15 +470,28 @@ def generate_adjust_ph_protocol_stepwise(
Returns:
List[Dict[str, Any]]: 动作序列
"""
action_sequence = []
debug_print("=" * 60)
debug_print(f"🔄 开始分步pH调节")
debug_print(f"📋 分步参数:")
debug_print(f" 🥼 vessel: {vessel}")
debug_print(f" 📊 ph_value: {ph_value}")
debug_print(f" 🧪 reagent: {reagent}")
debug_print(f" 📏 max_volume: {max_volume}mL")
debug_print(f" 🔢 steps: {steps}")
debug_print("=" * 60)
print(f"ADJUST_PH: 开始分步pH调节{steps}步)")
action_sequence = []
# 每步添加的体积
step_volume = max_volume / steps
debug_print(f"📊 每步体积: {step_volume:.2f}mL")
action_sequence.append(create_action_log(f"开始分步pH调节 ({steps}步)", "🔄"))
action_sequence.append(create_action_log(f"每步添加: {step_volume:.2f}mL", "📏"))
for i in range(steps):
print(f"ADJUST_PH: {i+1}/{steps} 步,添加 {step_volume} mL")
debug_print(f"🔄 执行{i+1}/{steps} 步,添加 {step_volume:.2f}mL")
action_sequence.append(create_action_log(f"{i+1}/{steps} 步开始", "🚀"))
# 生成单步协议
step_actions = generate_adjust_ph_protocol(
@@ -349,9 +507,13 @@ def generate_adjust_ph_protocol_stepwise(
)
action_sequence.extend(step_actions)
debug_print(f"✅ 第 {i+1}/{steps} 步完成,添加了 {len(step_actions)} 个动作")
action_sequence.append(create_action_log(f"{i+1}/{steps} 步完成", ""))
# 步骤间等待
if i < steps - 1:
debug_print(f"⏳ 步骤间等待30s")
action_sequence.append(create_action_log("步骤间等待...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
@@ -360,9 +522,10 @@ def generate_adjust_ph_protocol_stepwise(
}
})
print(f"ADJUST_PH: 分步pH调节完成")
return action_sequence
debug_print(f"🎉 分步pH调节完成{len(action_sequence)} 个动作")
action_sequence.append(create_action_log("分步pH调节全部完成", "🎉"))
return action_sequence
# 便捷函数常用pH调节
def generate_acidify_protocol(
@@ -372,11 +535,11 @@ def generate_acidify_protocol(
acid: str = "hydrochloric acid"
) -> List[Dict[str, Any]]:
"""酸化协议"""
debug_print(f"🍋 生成酸化协议: {vessel} → pH {target_ph} (使用 {acid})")
return generate_adjust_ph_protocol(
G, vessel, target_ph, acid, 0.0, True, 300.0, 120.0, 60.0
G, vessel, target_ph, acid
)
def generate_basify_protocol(
G: nx.DiGraph,
vessel: str,
@@ -384,28 +547,42 @@ def generate_basify_protocol(
base: str = "sodium hydroxide"
) -> List[Dict[str, Any]]:
"""碱化协议"""
debug_print(f"🧂 生成碱化协议: {vessel} → pH {target_ph} (使用 {base})")
return generate_adjust_ph_protocol(
G, vessel, target_ph, base, 0.0, True, 300.0, 120.0, 60.0
G, vessel, target_ph, base
)
def generate_neutralize_protocol(
G: nx.DiGraph,
vessel: str,
reagent: str = "sodium hydroxide"
) -> List[Dict[str, Any]]:
"""中和协议pH=7"""
debug_print(f"⚖️ 生成中和协议: {vessel} → pH 7.0 (使用 {reagent})")
return generate_adjust_ph_protocol(
G, vessel, 7.0, reagent, 0.0, True, 350.0, 180.0, 90.0
G, vessel, 7.0, reagent
)
# 测试函数
def test_adjust_ph_protocol():
"""测试pH调节协议"""
print("=== ADJUST PH PROTOCOL 测试 ===")
print("测试完成")
debug_print("=== ADJUST PH PROTOCOL 增强版测试 ===")
# 测试体积计算
debug_print("🧮 测试体积计算...")
test_cases = [
(2.0, "hydrochloric acid", 100.0),
(4.0, "hydrochloric acid", 100.0),
(12.0, "sodium hydroxide", 100.0),
(10.0, "sodium hydroxide", 100.0),
(7.0, "unknown reagent", 100.0)
]
for ph, reagent, volume in test_cases:
result = calculate_reagent_volume(ph, reagent, volume)
debug_print(f"📊 {reagent} → pH {ph}: {result:.2f}mL")
debug_print("✅ 测试完成")
if __name__ == "__main__":
test_adjust_ph_protocol()

View File

@@ -11,6 +11,22 @@ def debug_print(message):
print(f"[DISSOLVE] {message}", flush=True)
logger.info(f"[DISSOLVE] {message}")
# 🆕 创建进度日志动作
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
"""创建一个动作日志"""
full_message = f"{emoji} {message}"
debug_print(full_message)
logger.info(full_message)
print(f"[ACTION] {full_message}", flush=True)
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": full_message
}
}
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
解析体积输入,支持带单位的字符串
@@ -22,18 +38,20 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
debug_print(f"📏 体积输入为数值: {volume_input}")
return float(volume_input)
if not volume_input or not str(volume_input).strip():
debug_print(f"⚠️ 体积输入为空返回0.0mL")
return 0.0
volume_str = str(volume_input).lower().strip()
debug_print(f"解析体积输入: '{volume_str}'")
debug_print(f"🔍 解析体积输入: '{volume_str}'")
# 处理未知体积
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_volume = 50.0 # 默认50mL
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL 🎯")
return default_volume
# 移除空格并提取数字和单位
@@ -43,7 +61,7 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
if not match:
debug_print(f"⚠️ 无法解析体积: '{volume_str}'使用默认值50mL")
debug_print(f" 无法解析体积: '{volume_str}'使用默认值50mL")
return 50.0
value = float(match.group(1))
@@ -52,12 +70,14 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
# 转换为毫升
if unit in ['l', 'liter']:
volume = value * 1000.0 # L -> mL
debug_print(f"🔄 体积转换: {value}L → {volume}mL")
elif unit in ['μl', 'ul', 'microliter']:
volume = value / 1000.0 # μL -> mL
debug_print(f"🔄 体积转换: {value}μL → {volume}mL")
else: # ml, milliliter 或默认
volume = value # 已经是mL
debug_print(f"✅ 体积已为mL: {volume}mL")
debug_print(f"体积转换: {value}{unit}{volume}mL")
return volume
def parse_mass_input(mass_input: Union[str, float]) -> float:
@@ -71,18 +91,20 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
float: 质量(克)
"""
if isinstance(mass_input, (int, float)):
debug_print(f"⚖️ 质量输入为数值: {mass_input}g")
return float(mass_input)
if not mass_input or not str(mass_input).strip():
debug_print(f"⚠️ 质量输入为空返回0.0g")
return 0.0
mass_str = str(mass_input).lower().strip()
debug_print(f"解析质量输入: '{mass_str}'")
debug_print(f"🔍 解析质量输入: '{mass_str}'")
# 处理未知质量
if mass_str in ['?', 'unknown', 'tbd', 'to be determined']:
default_mass = 1.0 # 默认1g
debug_print(f"检测到未知质量,使用默认值: {default_mass}g")
debug_print(f"检测到未知质量,使用默认值: {default_mass}g 🎯")
return default_mass
# 移除空格并提取数字和单位
@@ -92,7 +114,7 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(g|mg|kg|gram|milligram|kilogram)?', mass_clean)
if not match:
debug_print(f"⚠️ 无法解析质量: '{mass_str}'返回0.0g")
debug_print(f" 无法解析质量: '{mass_str}'返回0.0g")
return 0.0
value = float(match.group(1))
@@ -101,12 +123,14 @@ def parse_mass_input(mass_input: Union[str, float]) -> float:
# 转换为克
if unit in ['mg', 'milligram']:
mass = value / 1000.0 # mg -> g
debug_print(f"🔄 质量转换: {value}mg → {mass}g")
elif unit in ['kg', 'kilogram']:
mass = value * 1000.0 # kg -> g
debug_print(f"🔄 质量转换: {value}kg → {mass}g")
else: # g, gram 或默认
mass = value # 已经是g
debug_print(f"✅ 质量已为g: {mass}g")
debug_print(f"质量转换: {value}{unit}{mass}g")
return mass
def parse_time_input(time_input: Union[str, float]) -> float:
@@ -120,18 +144,20 @@ def parse_time_input(time_input: Union[str, float]) -> float:
float: 时间(秒)
"""
if isinstance(time_input, (int, float)):
debug_print(f"⏱️ 时间输入为数值: {time_input}")
return float(time_input)
if not time_input or not str(time_input).strip():
debug_print(f"⚠️ 时间输入为空返回0秒")
return 0.0
time_str = str(time_input).lower().strip()
debug_print(f"解析时间输入: '{time_str}'")
debug_print(f"🔍 解析时间输入: '{time_str}'")
# 处理未知时间
if time_str in ['?', 'unknown', 'tbd']:
default_time = 600.0 # 默认10分钟
debug_print(f"检测到未知时间,使用默认值: {default_time}s")
debug_print(f"检测到未知时间,使用默认值: {default_time}s (10分钟) ⏰")
return default_time
# 移除空格并提取数字和单位
@@ -141,7 +167,7 @@ def parse_time_input(time_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(s|sec|second|min|minute|h|hr|hour|d|day)?', time_clean)
if not match:
debug_print(f"⚠️ 无法解析时间: '{time_str}'返回0s")
debug_print(f" 无法解析时间: '{time_str}'返回0s")
return 0.0
value = float(match.group(1))
@@ -150,14 +176,17 @@ def parse_time_input(time_input: Union[str, float]) -> float:
# 转换为秒
if unit in ['min', 'minute']:
time_sec = value * 60.0 # min -> s
debug_print(f"🔄 时间转换: {value}分钟 → {time_sec}")
elif unit in ['h', 'hr', 'hour']:
time_sec = value * 3600.0 # h -> s
debug_print(f"🔄 时间转换: {value}小时 → {time_sec}")
elif unit in ['d', 'day']:
time_sec = value * 86400.0 # d -> s
debug_print(f"🔄 时间转换: {value}天 → {time_sec}")
else: # s, sec, second 或默认
time_sec = value # 已经是s
debug_print(f"✅ 时间已为秒: {time_sec}")
debug_print(f"时间转换: {value}{unit}{time_sec}s")
return time_sec
def parse_temperature_input(temp_input: Union[str, float]) -> float:
@@ -171,13 +200,15 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
float: 温度(摄氏度)
"""
if isinstance(temp_input, (int, float)):
debug_print(f"🌡️ 温度输入为数值: {temp_input}°C")
return float(temp_input)
if not temp_input or not str(temp_input).strip():
debug_print(f"⚠️ 温度输入为空使用默认室温25°C")
return 25.0 # 默认室温
temp_str = str(temp_input).lower().strip()
debug_print(f"解析温度输入: '{temp_str}'")
debug_print(f"🔍 解析温度输入: '{temp_str}'")
# 处理特殊温度描述
temp_aliases = {
@@ -193,7 +224,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
if temp_str in temp_aliases:
result = temp_aliases[temp_str]
debug_print(f"温度别名解析: '{temp_str}'{result}°C")
debug_print(f"🏷️ 温度别名解析: '{temp_str}'{result}°C")
return result
# 移除空格并提取数字和单位
@@ -203,7 +234,7 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
match = re.match(r'([0-9]*\.?[0-9]+)\s*(°c|c|celsius|°f|f|fahrenheit|k|kelvin)?', temp_clean)
if not match:
debug_print(f"⚠️ 无法解析温度: '{temp_str}'使用默认值25°C")
debug_print(f" 无法解析温度: '{temp_str}'使用默认值25°C")
return 25.0
value = float(match.group(1))
@@ -212,19 +243,22 @@ def parse_temperature_input(temp_input: Union[str, float]) -> float:
# 转换为摄氏度
if unit in ['°f', 'f', 'fahrenheit']:
temp_c = (value - 32) * 5/9 # F -> C
debug_print(f"🔄 温度转换: {value}°F → {temp_c:.1f}°C")
elif unit in ['k', 'kelvin']:
temp_c = value - 273.15 # K -> C
debug_print(f"🔄 温度转换: {value}K → {temp_c:.1f}°C")
else: # °c, c, celsius 或默认
temp_c = value # 已经是C
debug_print(f"✅ 温度已为°C: {temp_c}°C")
debug_print(f"温度转换: {value}{unit}{temp_c}°C")
return temp_c
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""增强版溶剂容器查找"""
debug_print(f"查找溶剂 '{solvent}' 的容器...")
"""增强版溶剂容器查找,支持多种匹配模式"""
debug_print(f"🔍 开始查找溶剂 '{solvent}' 的容器...")
# 🔧 方法1直接搜索 data.reagent_name 和 config.reagent
debug_print(f"📋 方法1: 搜索reagent字段...")
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
@@ -237,18 +271,20 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
# 精确匹配
if reagent_name == solvent.lower() or config_reagent == solvent.lower():
debug_print(f"✅ 通过reagent字段到容器: {node}")
debug_print(f"✅ 通过reagent字段精确匹配到容器: {node} 🎯")
return node
# 模糊匹配
if (solvent.lower() in reagent_name and reagent_name) or \
(solvent.lower() in config_reagent and config_reagent):
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node} 🔍")
return node
# 🔧 方法2常见的容器命名规则
debug_print(f"📋 方法2: 使用命名规则查找...")
solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
solvent_clean,
f"flask_{solvent_clean}",
f"bottle_{solvent_clean}",
f"vessel_{solvent_clean}",
@@ -256,77 +292,118 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
f"{solvent_clean}_bottle",
f"solvent_{solvent_clean}",
f"reagent_{solvent_clean}",
f"reagent_bottle_{solvent_clean}"
f"reagent_bottle_{solvent_clean}",
f"reagent_bottle_1", # 通用试剂瓶
f"reagent_bottle_2",
f"reagent_bottle_3"
]
debug_print(f"🔍 尝试的容器名称: {possible_names[:5]}... (共{len(possible_names)}个)")
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
if node_type == 'container':
debug_print(f"✅ 通过命名规则找到容器: {name}")
debug_print(f"✅ 通过命名规则找到容器: {name} 📝")
return name
# 🔧 方法3使用第一个试剂瓶作为备选
# 🔧 方法3节点名称模糊匹配
debug_print(f"📋 方法3: 节点名称模糊匹配...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if node_data.get('type') == 'container':
# 检查节点名称是否包含溶剂名称
if solvent_clean in node_id.lower():
debug_print(f"✅ 通过节点名称模糊匹配到容器: {node_id} 🔍")
return node_id
# 检查液体类型匹配
vessel_data = node_data.get('data', {})
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == solvent.lower():
debug_print(f"✅ 通过液体类型匹配到容器: {node_id} 💧")
return node_id
# 🔧 方法4使用第一个试剂瓶作为备选
debug_print(f"📋 方法4: 查找备选试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower() or 'flask' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备选容器: {node_id}")
debug_print(f"⚠️ 未找到专用容器,使用备选试剂瓶: {node_id} 🔄")
return node_id
debug_print(f"❌ 所有方法都失败了,无法找到容器!")
raise ValueError(f"找不到溶剂 '{solvent}' 对应的容器")
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的加热搅拌器"""
debug_print(f"🔍 查找连接到容器 '{vessel}' 的加热搅拌器...")
heatchill_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'heatchill' in node_class:
heatchill_nodes.append(node)
debug_print(f"📋 发现加热搅拌器: {node}")
debug_print(f"📊 共找到 {len(heatchill_nodes)} 个加热搅拌器")
# 查找连接到容器的加热器
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
debug_print(f"找到连接的加热器: {heatchill}")
debug_print(f"找到连接的加热搅拌器: {heatchill} 🔗")
return heatchill
# 返回第一个加热器
if heatchill_nodes:
debug_print(f"使用第一个加热器: {heatchill_nodes[0]}")
debug_print(f"⚠️ 未找到直接连接的加热搅拌器,使用第一个: {heatchill_nodes[0]} 🔄")
return heatchill_nodes[0]
debug_print(f"❌ 未找到任何加热搅拌器")
return ""
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
debug_print(f"🔍 查找连接到容器 '{vessel}' 的搅拌器...")
stirrer_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'stirrer' in node_class:
stirrer_nodes.append(node)
debug_print(f"📋 发现搅拌器: {node}")
debug_print(f"📊 共找到 {len(stirrer_nodes)} 个搅拌器")
# 查找连接到容器的搅拌器
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"找到连接的搅拌器: {stirrer}")
debug_print(f"找到连接的搅拌器: {stirrer} 🔗")
return stirrer
# 返回第一个搅拌器
if stirrer_nodes:
debug_print(f"使用第一个搅拌器: {stirrer_nodes[0]}")
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个: {stirrer_nodes[0]} 🔄")
return stirrer_nodes[0]
debug_print(f"❌ 未找到任何搅拌器")
return ""
def find_solid_dispenser(G: nx.DiGraph) -> str:
"""查找固体加样器"""
debug_print(f"🔍 查找固体加样器...")
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'solid_dispenser' in node_class or 'dispenser' in node_class:
debug_print(f"找到固体加样器: {node}")
debug_print(f"找到固体加样器: {node} 🥄")
return node
debug_print("⚠️ 未找到固体加样器")
debug_print(f" 未找到固体加样器")
return ""
def generate_dissolve_protocol(
@@ -347,12 +424,13 @@ def generate_dissolve_protocol(
**kwargs # 🔧 关键接受所有其他参数防止unexpected keyword错误
) -> List[Dict[str, Any]]:
"""
生成溶解操作的协议序列 - 修复
生成溶解操作的协议序列 - 增强
🔧 修复要点:
1. 添加action文件中的所有参数mass, mol, reagent, event
2. 使用 **kwargs 接受所有额外参数,防止 unexpected keyword argument 错误
3. 支持固体溶解和液体溶解两种模式
4. 添加详细的emoji日志系统
支持两种溶解模式:
1. 液体溶解:指定 solvent + volume使用pump protocol转移溶剂
@@ -367,35 +445,40 @@ def generate_dissolve_protocol(
"""
debug_print("=" * 60)
debug_print("开始生成溶解协议 - 修复版")
debug_print(f"原始参数:")
debug_print(f" - vessel: '{vessel}'")
debug_print(f" - solvent: '{solvent}'")
debug_print(f" - volume: {volume} (类型: {type(volume)})")
debug_print(f" - mass: {mass} (类型: {type(mass)})")
debug_print(f" - temp: {temp} (类型: {type(temp)})")
debug_print(f" - time: {time} (类型: {type(time)})")
debug_print(f" - reagent: '{reagent}'")
debug_print(f" - mol: '{mol}'")
debug_print(f" - event: '{event}'")
debug_print(f" - kwargs: {kwargs}") # 显示额外参数
debug_print("🧪 开始生成溶解协议")
debug_print(f"📋 原始参数:")
debug_print(f" 🥼 vessel: '{vessel}'")
debug_print(f" 💧 solvent: '{solvent}'")
debug_print(f" 📏 volume: {volume} (类型: {type(volume)})")
debug_print(f" ⚖️ mass: {mass} (类型: {type(mass)})")
debug_print(f" 🌡️ temp: {temp} (类型: {type(temp)})")
debug_print(f" ⏱️ time: {time} (类型: {type(time)})")
debug_print(f" 🧪 reagent: '{reagent}'")
debug_print(f" 🧬 mol: '{mol}'")
debug_print(f" 🎯 event: '{event}'")
debug_print(f" 📦 kwargs: {kwargs}") # 显示额外参数
debug_print("=" * 60)
action_sequence = []
# === 参数验证 ===
debug_print("步骤1: 参数验证...")
debug_print("🔍 步骤1: 参数验证...")
action_sequence.append(create_action_log(f"开始溶解操作 - 容器: {vessel}", "🎬"))
if not vessel:
debug_print("❌ vessel 参数不能为空")
raise ValueError("vessel 参数不能为空")
if vessel not in G.nodes():
debug_print(f"❌ 容器 '{vessel}' 不存在于系统中")
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
debug_print("✅ 基本参数验证通过")
action_sequence.append(create_action_log("参数验证通过", ""))
# === 🔧 关键修复:参数解析 ===
debug_print("步骤2: 参数解析...")
debug_print("🔍 步骤2: 参数解析...")
action_sequence.append(create_action_log("正在解析溶解参数...", "🔍"))
# 解析各种参数为数值
final_volume = parse_volume_input(volume)
@@ -403,17 +486,18 @@ def generate_dissolve_protocol(
final_temp = parse_temperature_input(temp)
final_time = parse_time_input(time)
debug_print(f"解析结果:")
debug_print(f" - 体积: {final_volume}mL")
debug_print(f" - 质量: {final_mass}g")
debug_print(f" - 温度: {final_temp}°C")
debug_print(f" - 时间: {final_time}s")
debug_print(f" - 试剂: '{reagent}'")
debug_print(f" - 摩尔: '{mol}'")
debug_print(f" - 事件: '{event}'")
debug_print(f"📊 解析结果:")
debug_print(f" 📏 体积: {final_volume}mL")
debug_print(f" ⚖️ 质量: {final_mass}g")
debug_print(f" 🌡️ 温度: {final_temp}°C")
debug_print(f" ⏱️ 时间: {final_time}s")
debug_print(f" 🧪 试剂: '{reagent}'")
debug_print(f" 🧬 摩尔: '{mol}'")
debug_print(f" 🎯 事件: '{event}'")
# === 判断溶解类型 ===
debug_print("步骤3: 判断溶解类型...")
debug_print("🔍 步骤3: 判断溶解类型...")
action_sequence.append(create_action_log("正在判断溶解类型...", "🔍"))
# 判断是固体溶解还是液体溶解
is_solid_dissolve = (final_mass > 0 or (mol and mol.strip() != "") or (reagent and reagent.strip() != ""))
@@ -427,10 +511,15 @@ def generate_dissolve_protocol(
solvent = "water" # 默认溶剂
debug_print("⚠️ 未明确指定溶解参数默认为50mL水溶解")
debug_print(f"溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}")
dissolve_type = "固体溶解" if is_solid_dissolve else "液体溶解"
dissolve_emoji = "🧂" if is_solid_dissolve else "💧"
debug_print(f"📋 溶解类型: {dissolve_type} {dissolve_emoji}")
action_sequence.append(create_action_log(f"确定溶解类型: {dissolve_type} {dissolve_emoji}", "📋"))
# === 查找设备 ===
debug_print("步骤4: 查找设备...")
debug_print("🔍 步骤4: 查找设备...")
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找加热搅拌器
heatchill_id = find_connected_heatchill(G, vessel)
@@ -439,21 +528,31 @@ def generate_dissolve_protocol(
# 优先使用加热搅拌器,否则使用独立搅拌器
stir_device_id = heatchill_id or stirrer_id
debug_print(f"设备映射:")
debug_print(f" - 加热器: '{heatchill_id}'")
debug_print(f" - 搅拌器: '{stirrer_id}'")
debug_print(f" - 使用设备: '{stir_device_id}'")
debug_print(f"📊 设备映射:")
debug_print(f" 🔥 加热器: '{heatchill_id}'")
debug_print(f" 🌪️ 搅拌器: '{stirrer_id}'")
debug_print(f" 🎯 使用设备: '{stir_device_id}'")
if heatchill_id:
action_sequence.append(create_action_log(f"找到加热搅拌器: {heatchill_id}", "🔥"))
elif stirrer_id:
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️"))
else:
action_sequence.append(create_action_log("未找到搅拌设备,将跳过搅拌", "⚠️"))
# === 执行溶解流程 ===
debug_print("步骤5: 执行溶解流程...")
debug_print("🔍 步骤5: 执行溶解流程...")
try:
# 步骤5.1: 启动加热搅拌(如果需要)
if stir_device_id and (final_temp > 25.0 or final_time > 0 or stir_speed > 0):
debug_print(f"5.1: 启动加热搅拌,温度: {final_temp}°C")
debug_print(f"🔍 5.1: 启动加热搅拌,温度: {final_temp}°C")
action_sequence.append(create_action_log(f"准备加热搅拌 (目标温度: {final_temp}°C)", "🔥"))
if heatchill_id and (final_temp > 25.0 or final_time > 0):
# 使用加热搅拌器
action_sequence.append(create_action_log(f"启动加热搅拌器 {heatchill_id}", "🔥"))
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
@@ -468,6 +567,7 @@ def generate_dissolve_protocol(
# 等待温度稳定
if final_temp > 25.0:
wait_time = min(60, abs(final_temp - 25.0) * 1.5)
action_sequence.append(create_action_log(f"等待温度稳定 ({wait_time:.0f}秒)", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": wait_time}
@@ -475,6 +575,8 @@ def generate_dissolve_protocol(
elif stirrer_id:
# 使用独立搅拌器
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {stir_speed}rpm)", "🌪️"))
stir_action = {
"device_id": stirrer_id,
"action_name": "start_stir",
@@ -487,6 +589,7 @@ def generate_dissolve_protocol(
action_sequence.append(stir_action)
# 等待搅拌稳定
action_sequence.append(create_action_log("等待搅拌稳定...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
@@ -494,10 +597,13 @@ def generate_dissolve_protocol(
if is_solid_dissolve:
# === 固体溶解路径 ===
debug_print(f"5.2: 使用固体溶解路径")
debug_print(f"🔍 5.2: 使用固体溶解路径")
action_sequence.append(create_action_log("开始固体溶解流程", "🧂"))
solid_dispenser = find_solid_dispenser(G)
if solid_dispenser:
action_sequence.append(create_action_log(f"找到固体加样器: {solid_dispenser}", "🥄"))
# 固体加样
add_kwargs = {
"vessel": vessel,
@@ -508,9 +614,12 @@ def generate_dissolve_protocol(
if final_mass > 0:
add_kwargs["mass"] = str(final_mass)
action_sequence.append(create_action_log(f"准备添加固体: {final_mass}g", "⚖️"))
if mol and mol.strip():
add_kwargs["mol"] = mol
action_sequence.append(create_action_log(f"按摩尔数添加: {mol}", "🧬"))
action_sequence.append(create_action_log("开始固体加样操作", "🥄"))
action_sequence.append({
"device_id": solid_dispenser,
"action_name": "add_solid",
@@ -518,18 +627,24 @@ def generate_dissolve_protocol(
})
debug_print(f"✅ 固体加样完成")
action_sequence.append(create_action_log("固体加样完成", ""))
else:
debug_print("⚠️ 未找到固体加样器,跳过固体添加")
action_sequence.append(create_action_log("未找到固体加样器,无法添加固体", ""))
elif is_liquid_dissolve:
# === 液体溶解路径 ===
debug_print(f"5.3: 使用液体溶解路径")
debug_print(f"🔍 5.3: 使用液体溶解路径")
action_sequence.append(create_action_log("开始液体溶解流程", "💧"))
# 查找溶剂容器
action_sequence.append(create_action_log("正在查找溶剂容器...", "🔍"))
try:
solvent_vessel = find_solvent_vessel(G, solvent)
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "🧪"))
except ValueError as e:
debug_print(f"⚠️ {str(e)},跳过溶剂添加")
action_sequence.append(create_action_log(f"溶剂容器查找失败: {str(e)}", ""))
solvent_vessel = None
if solvent_vessel:
@@ -537,6 +652,9 @@ def generate_dissolve_protocol(
flowrate = 1.0 # 较慢的注入速度
transfer_flowrate = 0.5 # 较慢的转移速度
action_sequence.append(create_action_log(f"设置流速: {flowrate}mL/min (缓慢注入)", ""))
action_sequence.append(create_action_log(f"开始转移 {final_volume}mL {solvent}", "🚰"))
# 调用pump protocol
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
@@ -559,8 +677,10 @@ def generate_dissolve_protocol(
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 溶剂转移完成,添加了 {len(pump_actions)} 个动作")
action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", ""))
# 溶剂添加后等待
action_sequence.append(create_action_log("溶剂添加后短暂等待...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
@@ -568,10 +688,14 @@ def generate_dissolve_protocol(
# 步骤5.4: 等待溶解完成
if final_time > 0:
debug_print(f"5.4: 等待溶解完成 - {final_time}s")
debug_print(f"🔍 5.4: 等待溶解完成 - {final_time}s")
wait_minutes = final_time / 60
action_sequence.append(create_action_log(f"开始溶解等待 ({wait_minutes:.1f}分钟)", ""))
if heatchill_id:
# 使用定时加热搅拌
action_sequence.append(create_action_log(f"使用加热搅拌器进行定时溶解", "🔥"))
dissolve_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
@@ -588,6 +712,8 @@ def generate_dissolve_protocol(
elif stirrer_id:
# 使用定时搅拌
action_sequence.append(create_action_log(f"使用搅拌器进行定时溶解", "🌪️"))
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
@@ -603,6 +729,7 @@ def generate_dissolve_protocol(
else:
# 简单等待
action_sequence.append(create_action_log(f"简单等待溶解完成", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": final_time}
@@ -610,7 +737,8 @@ def generate_dissolve_protocol(
# 步骤5.5: 停止加热搅拌(如果需要)
if heatchill_id and final_time == 0 and final_temp > 25.0:
debug_print(f"5.5: 停止加热器")
debug_print(f"🔍 5.5: 停止加热器")
action_sequence.append(create_action_log("停止加热搅拌器", "🛑"))
stop_action = {
"device_id": heatchill_id,
@@ -622,7 +750,8 @@ def generate_dissolve_protocol(
action_sequence.append(stop_action)
except Exception as e:
debug_print(f"⚠️ 溶解流程执行失败: {str(e)}")
debug_print(f" 溶解流程执行失败: {str(e)}")
action_sequence.append(create_action_log(f"溶解流程失败: {str(e)}", ""))
# 添加错误日志
action_sequence.append({
"device_id": "system",
@@ -634,21 +763,30 @@ def generate_dissolve_protocol(
# === 最终结果 ===
debug_print("=" * 60)
debug_print(f" 溶解协议生成完成")
debug_print(f"📊 总动作数: {len(action_sequence)}")
debug_print(f"📋 处理总结:")
debug_print(f" - 容器: {vessel}")
debug_print(f" - 溶解类型: {'固体溶解' if is_solid_dissolve else '液体溶解'}")
debug_print(f"🎉 溶解协议生成完成")
debug_print(f"📊 协议统计:")
debug_print(f" 📋 总动作数: {len(action_sequence)}")
debug_print(f" 🥼 容器: {vessel}")
debug_print(f" {dissolve_emoji} 溶解类型: {dissolve_type}")
if is_liquid_dissolve:
debug_print(f" - 溶剂: {solvent} ({final_volume}mL)")
debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)")
if is_solid_dissolve:
debug_print(f" - 试剂: {reagent}")
debug_print(f" - 质量: {final_mass}g")
debug_print(f" - 摩尔: {mol}")
debug_print(f" - 温度: {final_temp}°C")
debug_print(f" - 时间: {final_time}s")
debug_print(f" 🧪 试剂: {reagent}")
debug_print(f" ⚖️ 质量: {final_mass}g")
debug_print(f" 🧬 摩尔: {mol}")
debug_print(f" 🌡️ 温度: {final_temp}°C")
debug_print(f" ⏱️ 时间: {final_time}s")
debug_print("=" * 60)
# 添加完成日志
summary_msg = f"溶解协议完成: {vessel}"
if is_liquid_dissolve:
summary_msg += f" (使用 {final_volume}mL {solvent})"
if is_solid_dissolve:
summary_msg += f" (溶解 {final_mass}g {reagent})"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
# === 便捷函数 ===
@@ -656,6 +794,7 @@ def generate_dissolve_protocol(
def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union[str, float],
temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
"""按质量溶解固体"""
debug_print(f"🧂 快速固体溶解: {reagent} ({mass}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
mass=mass,
@@ -667,6 +806,7 @@ def dissolve_solid_by_mass(G: nx.DiGraph, vessel: str, reagent: str, mass: Union
def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
temp: Union[str, float] = 25.0, time: Union[str, float] = "10 min") -> List[Dict[str, Any]]:
"""按摩尔数溶解固体"""
debug_print(f"🧬 按摩尔数溶解固体: {reagent} ({mol}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
mol=mol,
@@ -678,6 +818,7 @@ def dissolve_solid_by_moles(G: nx.DiGraph, vessel: str, reagent: str, mol: str,
def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
temp: Union[str, float] = 25.0, time: Union[str, float] = "5 min") -> List[Dict[str, Any]]:
"""用溶剂溶解"""
debug_print(f"💧 溶剂溶解: {solvent} ({volume}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
@@ -688,6 +829,7 @@ def dissolve_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio
def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float]) -> List[Dict[str, Any]]:
"""室温溶解"""
debug_print(f"🌡️ 室温溶解: {solvent} ({volume}) → {vessel}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
@@ -699,6 +841,7 @@ def dissolve_at_room_temp(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio
def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
temp: Union[str, float] = "60 °C", time: Union[str, float] = "15 min") -> List[Dict[str, Any]]:
"""加热溶解"""
debug_print(f"🔥 加热溶解: {solvent} ({volume}) → {vessel} @ {temp}")
return generate_dissolve_protocol(
G, vessel,
solvent=solvent,
@@ -710,33 +853,37 @@ def dissolve_with_heating(G: nx.DiGraph, vessel: str, solvent: str, volume: Unio
# 测试函数
def test_dissolve_protocol():
"""测试溶解协议的各种参数解析"""
print("=== DISSOLVE PROTOCOL 修复版测试 ===")
debug_print("=== DISSOLVE PROTOCOL 增强版测试 ===")
# 测试体积解析
debug_print("💧 测试体积解析...")
volumes = ["10 mL", "?", 10.0, "1 L", "500 μL"]
for vol in volumes:
result = parse_volume_input(vol)
print(f"体积解析: {vol}{result}mL")
debug_print(f"📏 体积解析: {vol}{result}mL")
# 测试质量解析
debug_print("⚖️ 测试质量解析...")
masses = ["2.9 g", "?", 2.5, "500 mg"]
for mass in masses:
result = parse_mass_input(mass)
print(f"质量解析: {mass}{result}g")
debug_print(f"⚖️ 质量解析: {mass}{result}g")
# 测试温度解析
debug_print("🌡️ 测试温度解析...")
temps = ["60 °C", "room temperature", "?", 25.0, "reflux"]
for temp in temps:
result = parse_temperature_input(temp)
print(f"温度解析: {temp}{result}°C")
debug_print(f"🌡️ 温度解析: {temp}{result}°C")
# 测试时间解析
debug_print("⏱️ 测试时间解析...")
times = ["30 min", "1 h", "?", 60.0]
for time in times:
result = parse_time_input(time)
print(f"时间解析: {time}{result}s")
debug_print(f"⏱️ 时间解析: {time}{result}s")
print("✅ 测试完成")
debug_print("✅ 测试完成")
if __name__ == "__main__":
test_dissolve_protocol()

View File

@@ -1,16 +1,68 @@
import networkx as nx
import logging
import uuid # 🔧 移到顶部
import uuid
import sys
from typing import List, Dict, Any, Optional
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
# 设置日志
logger = logging.getLogger(__name__)
# 确保输出编码为UTF-8
if hasattr(sys.stdout, 'reconfigure'):
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except:
pass
def debug_print(message):
"""调试输出函数"""
print(f"[EVACUATE_REFILL] {message}", flush=True)
logger.info(f"[EVACUATE_REFILL] {message}")
"""调试输出函数 - 支持中文"""
try:
# 确保消息是字符串格式
safe_message = str(message)
print(f"[抽真空充气] {safe_message}", flush=True)
logger.info(f"[抽真空充气] {safe_message}")
except UnicodeEncodeError:
# 如果编码失败,尝试替换不支持的字符
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
print(f"[抽真空充气] {safe_message}", flush=True)
logger.info(f"[抽真空充气] {safe_message}")
except Exception as e:
# 最后的安全措施
fallback_message = f"日志输出错误: {repr(message)}"
print(f"[抽真空充气] {fallback_message}", flush=True)
logger.info(f"[抽真空充气] {fallback_message}")
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
"""创建一个动作日志 - 支持中文和emoji"""
try:
full_message = f"{emoji} {message}"
debug_print(full_message)
logger.info(full_message)
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": full_message,
"progress_message": full_message
}
}
except Exception as e:
# 如果emoji有问题使用纯文本
safe_message = f"[日志] {message}"
debug_print(safe_message)
logger.info(safe_message)
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": safe_message,
"progress_message": safe_message
}
}
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
"""
@@ -19,9 +71,10 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
2. 气体类型匹配data.gas_type
3. 默认气源
"""
debug_print(f"正在查找气体 '{gas}' 的气源...")
debug_print(f"🔍 正在查找气体 '{gas}' 的气源...")
# 第一步:通过容器名称匹配
debug_print(f"📋 方法1: 容器名称匹配...")
gas_source_patterns = [
f"gas_source_{gas}",
f"gas_{gas}",
@@ -32,12 +85,15 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
f"bottle_{gas}"
]
debug_print(f"🎯 尝试的容器名称: {gas_source_patterns}")
for pattern in gas_source_patterns:
if pattern in G.nodes():
debug_print(f"通过名称匹配找到气源: {pattern}")
debug_print(f"通过名称找到气源: {pattern}")
return pattern
# 第二步:通过气体类型匹配 (data.gas_type)
debug_print(f"📋 方法2: 气体类型匹配...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
node_class = node_data.get('class', '') or ''
@@ -52,7 +108,7 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
gas_type = data.get('gas_type', '')
if gas_type.lower() == gas.lower():
debug_print(f"通过气体类型匹配找到气源: {node_id} (gas_type: {gas_type})")
debug_print(f"通过气体类型找到气源: {node_id} (气体类型: {gas_type})")
return node_id
# 检查 config.gas_type
@@ -60,10 +116,11 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
config_gas_type = config.get('gas_type', '')
if config_gas_type.lower() == gas.lower():
debug_print(f"通过配置气体类型匹配找到气源: {node_id} (config.gas_type: {config_gas_type})")
debug_print(f"通过配置气体类型找到气源: {node_id} (配置气体类型: {config_gas_type})")
return node_id
# 第三步:查找所有可用的气源设备
debug_print(f"📋 方法3: 查找可用气源...")
available_gas_sources = []
for node_id in G.nodes():
node_data = G.nodes[node_id]
@@ -74,12 +131,13 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
(node_id.startswith('flask_') and any(g in node_id.lower() for g in ['air', 'nitrogen', 'argon']))):
data = node_data.get('data', {})
gas_type = data.get('gas_type', 'unknown')
available_gas_sources.append(f"{node_id} (gas_type: {gas_type})")
gas_type = data.get('gas_type', '未知')
available_gas_sources.append(f"{node_id} (气体类型: {gas_type})")
debug_print(f"可用气源列表: {available_gas_sources}")
debug_print(f"📊 可用气源: {available_gas_sources}")
# 第四步:如果找不到特定气体,使用默认的第一个气源
debug_print(f"📋 方法4: 查找默认气源...")
default_gas_sources = [
node for node in G.nodes()
if ((G.nodes[node].get('class') or '').find('virtual_gas_source') != -1
@@ -91,11 +149,12 @@ def find_gas_source(G: nx.DiGraph, gas: str) -> str:
debug_print(f"⚠️ 未找到特定气体 '{gas}',使用默认气源: {default_source}")
return default_source
raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}")
debug_print(f"❌ 所有方法都失败了!")
raise ValueError(f"无法找到气体 '{gas}' 的气源。可用气源: {available_gas_sources}")
def find_vacuum_pump(G: nx.DiGraph) -> str:
"""查找真空泵设备"""
debug_print("查找真空泵设备...")
debug_print("🔍 正在查找真空泵...")
vacuum_pumps = []
for node in G.nodes():
@@ -106,16 +165,18 @@ def find_vacuum_pump(G: nx.DiGraph) -> str:
'vacuum_pump' in node.lower() or
'vacuum' in node_class.lower()):
vacuum_pumps.append(node)
debug_print(f"📋 发现真空泵: {node}")
if not vacuum_pumps:
raise ValueError("系统中未找到真空泵设备")
debug_print(f"系统中未找到真空泵")
raise ValueError("系统中未找到真空泵")
debug_print(f"找到真空泵: {vacuum_pumps[0]}")
debug_print(f"✅ 使用真空泵: {vacuum_pumps[0]}")
return vacuum_pumps[0]
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]:
"""查找与指定容器相连的搅拌器"""
debug_print(f"查找与容器 {vessel} 连的搅拌器...")
debug_print(f"🔍 正在查找与容器 {vessel}的搅拌器...")
stirrer_nodes = []
for node in G.nodes():
@@ -124,24 +185,27 @@ def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> Optional[str]:
if 'virtual_stirrer' in node_class or 'stirrer' in node.lower():
stirrer_nodes.append(node)
debug_print(f"📋 发现搅拌器: {node}")
debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}")
# 检查哪个搅拌器与目标容器相连
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"找到连接的搅拌器: {stirrer}")
debug_print(f"找到连接的搅拌器: {stirrer}")
return stirrer
# 如果没有连接的搅拌器,返回第一个可用的
if stirrer_nodes:
debug_print(f"未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
return stirrer_nodes[0]
debug_print("未找到搅拌器")
debug_print("未找到搅拌器")
return None
def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]:
"""查找真空泵相关的电磁阀 - 根据实际连接逻辑"""
debug_print(f"查找真空泵 {vacuum_pump} 相关的电磁阀...")
"""查找真空泵相关的电磁阀"""
debug_print(f"🔍 正在查找真空泵 {vacuum_pump} 的电磁阀...")
# 查找所有电磁阀
solenoid_valves = []
@@ -151,29 +215,30 @@ def find_vacuum_solenoid_valve(G: nx.DiGraph, vacuum_pump: str) -> Optional[str]
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
solenoid_valves.append(node)
debug_print(f"📋 发现电磁阀: {node}")
debug_print(f"找到的电磁阀: {solenoid_valves}")
debug_print(f"📊 找到的电磁阀: {solenoid_valves}")
# 🔧 修复:根据实际组态图连接逻辑查找
# vacuum_pump_1 <- solenoid_valve_1 <- multiway_valve_2
# 检查连接关系
debug_print(f"📋 方法1: 检查连接关系...")
for solenoid in solenoid_valves:
# 检查电磁阀是否连接到真空泵
if G.has_edge(solenoid, vacuum_pump) or G.has_edge(vacuum_pump, solenoid):
debug_print(f" 找到连接真空泵的电磁阀: {solenoid}")
debug_print(f" 找到连接真空电磁阀: {solenoid}")
return solenoid
# 通过命名规则查找(备选方案)
# 通过命名规则查找
debug_print(f"📋 方法2: 检查命名规则...")
for solenoid in solenoid_valves:
if 'vacuum' in solenoid.lower() or solenoid == 'solenoid_valve_1':
debug_print(f" 通过命名规则找到真空电磁阀: {solenoid}")
debug_print(f" 通过命名找到真空电磁阀: {solenoid}")
return solenoid
debug_print("⚠️ 未找到真空电磁阀")
return None
def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]:
"""查找气源相关的电磁阀 - 根据实际连接逻辑"""
debug_print(f"查找气源 {gas_source} 相关的电磁阀...")
"""查找气源相关的电磁阀"""
debug_print(f"🔍 正在查找气源 {gas_source} 的电磁阀...")
# 查找所有电磁阀
solenoid_valves = []
@@ -184,18 +249,20 @@ def find_gas_solenoid_valve(G: nx.DiGraph, gas_source: str) -> Optional[str]:
if ('solenoid' in node_class.lower() or 'solenoid_valve' in node.lower()):
solenoid_valves.append(node)
# 🔧 修复:根据实际组态图连接逻辑查找
# gas_source_1 -> solenoid_valve_2 -> multiway_valve_2
debug_print(f"📊 找到的电磁阀: {solenoid_valves}")
# 检查连接关系
debug_print(f"📋 方法1: 检查连接关系...")
for solenoid in solenoid_valves:
# 检查气源是否连接到电磁阀
if G.has_edge(gas_source, solenoid) or G.has_edge(solenoid, gas_source):
debug_print(f" 找到连接气源电磁阀: {solenoid}")
debug_print(f" 找到连接气源电磁阀: {solenoid}")
return solenoid
# 通过命名规则查找(备选方案)
# 通过命名规则查找
debug_print(f"📋 方法2: 检查命名规则...")
for solenoid in solenoid_valves:
if 'gas' in solenoid.lower() or solenoid == 'solenoid_valve_2':
debug_print(f" 通过命名规则找到气源电磁阀: {solenoid}")
debug_print(f" 通过命名找到气源电磁阀: {solenoid}")
return solenoid
debug_print("⚠️ 未找到气源电磁阀")
@@ -208,7 +275,7 @@ def generate_evacuateandrefill_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""
生成抽真空和充气操作的动作序列 - 最终修复版本
生成抽真空和充气操作的动作序列 - 中文版
Args:
G: 设备图
@@ -223,75 +290,113 @@ def generate_evacuateandrefill_protocol(
# 硬编码重复次数为 3
repeats = 3
# 🔧 修复:在函数开始就生成协议ID
# 生成协议ID
protocol_id = str(uuid.uuid4())
debug_print(f"生成协议ID: {protocol_id}")
debug_print(f"🆔 生成协议ID: {protocol_id}")
debug_print("=" * 60)
debug_print("开始生成抽真空充气协议")
debug_print(f"输入参数:")
debug_print(f" - vessel: {vessel}")
debug_print(f" - gas: {gas}")
debug_print(f" - repeats: {repeats} (硬编码)")
debug_print(f" - 其他参数: {kwargs}")
debug_print("🧪 开始生成抽真空充气协议")
debug_print(f"📋 原始参数:")
debug_print(f" 🥼 容器: '{vessel}'")
debug_print(f" 💨 气体: '{gas}'")
debug_print(f" 🔄 循环次数: {repeats} (硬编码)")
debug_print(f" 📦 其他参数: {kwargs}")
debug_print("=" * 60)
action_sequence = []
# === 参数验证和修正 ===
debug_print("步骤1: 参数验证和修正...")
debug_print("🔍 步骤1: 参数验证和修正...")
action_sequence.append(create_action_log(f"开始抽真空充气操作 - 容器: {vessel}", "🎬"))
action_sequence.append(create_action_log(f"目标气体: {gas}", "💨"))
action_sequence.append(create_action_log(f"循环次数: {repeats}", "🔄"))
# 验证必需参数
if not vessel:
raise ValueError("vessel 参数不能为空")
debug_print("❌ 容器参数不能为空")
raise ValueError("容器参数不能为空")
if not gas:
raise ValueError("gas 参数不能为空")
debug_print("❌ 气体参数不能为空")
raise ValueError("气体参数不能为空")
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
debug_print(f"容器 '{vessel}' 在系统中不存在")
raise ValueError(f"容器 '{vessel}' 在系统中不存在")
debug_print("✅ 基本参数验证通过")
action_sequence.append(create_action_log("参数验证通过", ""))
# 标准化气体名称
debug_print("🔧 标准化气体名称...")
gas_aliases = {
'n2': 'nitrogen',
'ar': 'argon',
'air': 'air',
'o2': 'oxygen',
'co2': 'carbon_dioxide',
'h2': 'hydrogen'
'h2': 'hydrogen',
'氮气': 'nitrogen',
'氩气': 'argon',
'空气': 'air',
'氧气': 'oxygen',
'二氧化碳': 'carbon_dioxide',
'氢气': 'hydrogen'
}
original_gas = gas
gas_lower = gas.lower().strip()
if gas_lower in gas_aliases:
gas = gas_aliases[gas_lower]
debug_print(f"标准化气体名称: {original_gas} -> {gas}")
debug_print(f"🔄 标准化气体名称: {original_gas} -> {gas}")
action_sequence.append(create_action_log(f"气体名称标准化: {original_gas} -> {gas}", "🔄"))
debug_print(f"最终参数: vessel={vessel}, gas={gas}, repeats={repeats}")
debug_print(f"📋 最终参数: 容器={vessel}, 气体={gas}, 重复={repeats}")
# === 查找设备 ===
debug_print("步骤2: 查找设备...")
debug_print("🔍 步骤2: 查找设备...")
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
try:
vacuum_pump = find_vacuum_pump(G)
gas_source = find_gas_source(G, gas)
vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump)
gas_solenoid = find_gas_solenoid_valve(G, gas_source)
stirrer_id = find_connected_stirrer(G, vessel)
action_sequence.append(create_action_log(f"找到真空泵: {vacuum_pump}", "🌪️"))
debug_print(f"设备配置:")
debug_print(f" - 真空泵: {vacuum_pump}")
debug_print(f" - 气源: {gas_source}")
debug_print(f" - 真空电磁阀: {vacuum_solenoid}")
debug_print(f" - 气源电磁阀: {gas_solenoid}")
debug_print(f" - 搅拌器: {stirrer_id}")
gas_source = find_gas_source(G, gas)
action_sequence.append(create_action_log(f"找到气源: {gas_source}", "💨"))
vacuum_solenoid = find_vacuum_solenoid_valve(G, vacuum_pump)
if vacuum_solenoid:
action_sequence.append(create_action_log(f"找到真空电磁阀: {vacuum_solenoid}", "🚪"))
else:
action_sequence.append(create_action_log("未找到真空电磁阀", "⚠️"))
gas_solenoid = find_gas_solenoid_valve(G, gas_source)
if gas_solenoid:
action_sequence.append(create_action_log(f"找到气源电磁阀: {gas_solenoid}", "🚪"))
else:
action_sequence.append(create_action_log("未找到气源电磁阀", "⚠️"))
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_id}", "🌪️"))
else:
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
debug_print(f"📊 设备配置:")
debug_print(f" 🌪️ 真空泵: {vacuum_pump}")
debug_print(f" 💨 气源: {gas_source}")
debug_print(f" 🚪 真空电磁阀: {vacuum_solenoid}")
debug_print(f" 🚪 气源电磁阀: {gas_solenoid}")
debug_print(f" 🌪️ 搅拌器: {stirrer_id}")
except Exception as e:
debug_print(f"❌ 设备查找失败: {str(e)}")
action_sequence.append(create_action_log(f"设备查找失败: {str(e)}", ""))
raise ValueError(f"设备查找失败: {str(e)}")
# === 参数设置 ===
debug_print("步骤3: 参数设置...")
debug_print("🔍 步骤3: 参数设置...")
action_sequence.append(create_action_log("设置操作参数...", "⚙️"))
# 根据气体类型调整参数
if gas.lower() in ['nitrogen', 'argon']:
@@ -300,87 +405,108 @@ def generate_evacuateandrefill_protocol(
PUMP_FLOW_RATE = 2.0
VACUUM_TIME = 30.0
REFILL_TIME = 20.0
debug_print("惰性气体使用标准参数")
debug_print("💨 惰性气体: 使用标准参数")
action_sequence.append(create_action_log("检测到惰性气体,使用标准参数", "💨"))
elif gas.lower() in ['air', 'oxygen']:
VACUUM_VOLUME = 20.0
REFILL_VOLUME = 20.0
PUMP_FLOW_RATE = 1.5
VACUUM_TIME = 45.0
REFILL_TIME = 25.0
debug_print("活性气体使用保守参数")
debug_print("🔥 活性气体: 使用保守参数")
action_sequence.append(create_action_log("检测到活性气体,使用保守参数", "🔥"))
else:
VACUUM_VOLUME = 15.0
REFILL_VOLUME = 15.0
PUMP_FLOW_RATE = 1.0
VACUUM_TIME = 60.0
REFILL_TIME = 30.0
debug_print("未知气体使用安全参数")
debug_print("未知气体: 使用安全参数")
action_sequence.append(create_action_log("未知气体类型,使用安全参数", ""))
STIR_SPEED = 200.0
debug_print(f"操作参数:")
debug_print(f" - 抽真空体积: {VACUUM_VOLUME}mL")
debug_print(f" - 充气体积: {REFILL_VOLUME}mL")
debug_print(f" - 泵流速: {PUMP_FLOW_RATE}mL/s")
debug_print(f" - 抽真空时间: {VACUUM_TIME}s")
debug_print(f" - 充气时间: {REFILL_TIME}s")
debug_print(f" - 搅拌速度: {STIR_SPEED}RPM")
debug_print(f"⚙️ 操作参数:")
debug_print(f" 📏 真空体积: {VACUUM_VOLUME}mL")
debug_print(f" 📏 充气体积: {REFILL_VOLUME}mL")
debug_print(f" 泵流速: {PUMP_FLOW_RATE}mL/s")
debug_print(f" ⏱️ 真空时间: {VACUUM_TIME}s")
debug_print(f" ⏱️ 充气时间: {REFILL_TIME}s")
debug_print(f" 🌪️ 搅拌速度: {STIR_SPEED}RPM")
action_sequence.append(create_action_log(f"真空体积: {VACUUM_VOLUME}mL", "📏"))
action_sequence.append(create_action_log(f"充气体积: {REFILL_VOLUME}mL", "📏"))
action_sequence.append(create_action_log(f"泵流速: {PUMP_FLOW_RATE}mL/s", ""))
# === 路径验证 ===
debug_print("步骤4: 路径验证...")
debug_print("🔍 步骤4: 路径验证...")
action_sequence.append(create_action_log("验证传输路径...", "🛤️"))
try:
# 验证抽真空路径: vessel -> vacuum_pump (通过八通阀和电磁阀)
# 验证抽真空路径
if nx.has_path(G, vessel, vacuum_pump):
vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
debug_print(f"真空路径: {' '.join(vacuum_path)}")
debug_print(f"真空路径: {' -> '.join(vacuum_path)}")
action_sequence.append(create_action_log(f"真空路径: {' -> '.join(vacuum_path)}", "🛤️"))
else:
debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题")
debug_print(f"⚠️ 真空路径不存在,继续执行但可能有问题")
action_sequence.append(create_action_log("真空路径检查: 路径不存在", "⚠️"))
# 验证充气路径: gas_source -> vessel (通过电磁阀和八通阀)
# 验证充气路径
if nx.has_path(G, gas_source, vessel):
gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
debug_print(f"充气路径: {' '.join(gas_path)}")
debug_print(f"✅ 气体路径: {' -> '.join(gas_path)}")
action_sequence.append(create_action_log(f"气体路径: {' -> '.join(gas_path)}", "🛤️"))
else:
debug_print(f"⚠️ 气路径不存在,继续执行但可能有问题")
debug_print(f"⚠️ 气路径不存在,继续执行但可能有问题")
action_sequence.append(create_action_log("气体路径检查: 路径不存在", "⚠️"))
except Exception as e:
debug_print(f"⚠️ 路径验证失败: {str(e)},继续执行")
action_sequence.append(create_action_log(f"路径验证失败: {str(e)}", "⚠️"))
# === 启动搅拌器 ===
debug_print("步骤5: 启动搅拌器...")
debug_print("🔍 步骤5: 启动搅拌器...")
if stirrer_id:
debug_print(f"启动搅拌器: {stirrer_id}")
debug_print(f"🌪️ 启动搅拌器: {stirrer_id}")
action_sequence.append(create_action_log(f"启动搅拌器 {stirrer_id} (速度: {STIR_SPEED}rpm)", "🌪️"))
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": STIR_SPEED,
"purpose": "抽真空充气操作前启动搅拌"
"purpose": "抽真空充气前预搅拌"
}
})
# 等待搅拌稳定
action_sequence.append(create_action_log("等待搅拌稳定...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5.0}
})
else:
debug_print("未找到搅拌器,跳过搅拌启动")
debug_print("⚠️ 未找到搅拌器,跳过搅拌启动")
action_sequence.append(create_action_log("跳过搅拌器启动", "⏭️"))
# === 执行 3 次抽真空-充气循环 ===
debug_print("步骤6: 执行抽真空-充气循环...")
# === 执行循环 ===
debug_print("🔍 步骤6: 执行抽真空-充气循环...")
action_sequence.append(create_action_log(f"开始 {repeats} 次抽真空-充气循环", "🔄"))
for cycle in range(repeats):
debug_print(f"=== 第 {cycle+1}/{repeats} 循环 ===")
debug_print(f"=== 第 {cycle+1}/{repeats} 循环 ===")
action_sequence.append(create_action_log(f"{cycle+1}/{repeats} 轮循环开始", "🚀"))
# ============ 抽真空阶段 ============
debug_print(f"抽真空阶段开始")
debug_print(f"🌪️ 抽真空阶段开始")
action_sequence.append(create_action_log("开始抽真空阶段", "🌪️"))
# 启动真空泵
debug_print(f"启动真空泵: {vacuum_pump}")
debug_print(f"🔛 启动真空泵: {vacuum_pump}")
action_sequence.append(create_action_log(f"启动真空泵: {vacuum_pump}", "🔛"))
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
@@ -389,17 +515,19 @@ def generate_evacuateandrefill_protocol(
# 开启真空电磁阀
if vacuum_solenoid:
debug_print(f"真空电磁阀: {vacuum_solenoid}")
debug_print(f"🚪 打开真空电磁阀: {vacuum_solenoid}")
action_sequence.append(create_action_log(f"打开真空电磁阀: {vacuum_solenoid}", "🚪"))
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
# 抽真空操作 - 使用液体转移协议
debug_print(f"抽真空操作: {vessel} {vacuum_pump}")
try:
# 抽真空操作
debug_print(f"🌪️ 抽真空操作: {vessel} -> {vacuum_pump}")
action_sequence.append(create_action_log(f"开始抽真空: {vessel} -> {vacuum_pump}", "🌪️"))
try:
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=vessel,
@@ -419,8 +547,10 @@ def generate_evacuateandrefill_protocol(
if vacuum_transfer_actions:
action_sequence.extend(vacuum_transfer_actions)
debug_print(f"✅ 添加了 {len(vacuum_transfer_actions)} 个抽真空动作")
action_sequence.append(create_action_log(f"抽真空协议完成 ({len(vacuum_transfer_actions)} 个操作)", ""))
else:
debug_print("⚠️ 抽真空协议返回空序列,添加手动动作")
action_sequence.append(create_action_log("抽真空协议为空,使用手动等待", "⚠️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": VACUUM_TIME}
@@ -428,13 +558,15 @@ def generate_evacuateandrefill_protocol(
except Exception as e:
debug_print(f"❌ 抽真空失败: {str(e)}")
# 添加等待时间作为备选
action_sequence.append(create_action_log(f"抽真空失败: {str(e)}", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": VACUUM_TIME}
})
# 抽真空后等待
wait_minutes = VACUUM_TIME / 60
action_sequence.append(create_action_log(f"抽真空后等待 ({wait_minutes:.1f} 分钟)", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": VACUUM_TIME}
@@ -442,7 +574,8 @@ def generate_evacuateandrefill_protocol(
# 关闭真空电磁阀
if vacuum_solenoid:
debug_print(f"关闭真空电磁阀: {vacuum_solenoid}")
debug_print(f"🚪 关闭真空电磁阀: {vacuum_solenoid}")
action_sequence.append(create_action_log(f"关闭真空电磁阀: {vacuum_solenoid}", "🚪"))
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
@@ -450,24 +583,28 @@ def generate_evacuateandrefill_protocol(
})
# 关闭真空泵
debug_print(f"关闭真空泵: {vacuum_pump}")
debug_print(f"🔴 停止真空泵: {vacuum_pump}")
action_sequence.append(create_action_log(f"停止真空泵: {vacuum_pump}", "🔴"))
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
"action_kwargs": {"string": "OFF"}
})
# 抽真空后等待
# 阶段间等待
action_sequence.append(create_action_log("抽真空阶段完成,短暂等待", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5.0}
})
# ============ 充气阶段 ============
debug_print(f"充气阶段开始")
debug_print(f"💨 充气阶段开始")
action_sequence.append(create_action_log("开始气体充气阶段", "💨"))
# 启动气源
debug_print(f"启动气源: {gas_source}")
debug_print(f"🔛 启动气源: {gas_source}")
action_sequence.append(create_action_log(f"启动气源: {gas_source}", "🔛"))
action_sequence.append({
"device_id": gas_source,
"action_name": "set_status",
@@ -476,17 +613,19 @@ def generate_evacuateandrefill_protocol(
# 开启气源电磁阀
if gas_solenoid:
debug_print(f"气源电磁阀: {gas_solenoid}")
debug_print(f"🚪 打开气源电磁阀: {gas_solenoid}")
action_sequence.append(create_action_log(f"打开气源电磁阀: {gas_solenoid}", "🚪"))
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
# 充气操作 - 使用液体转移协议
debug_print(f"充气操作: {gas_source} {vessel}")
try:
# 充气操作
debug_print(f"💨 充气操作: {gas_source} -> {vessel}")
action_sequence.append(create_action_log(f"开始气体充气: {gas_source} -> {vessel}", "💨"))
try:
gas_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=gas_source,
@@ -506,22 +645,26 @@ def generate_evacuateandrefill_protocol(
if gas_transfer_actions:
action_sequence.extend(gas_transfer_actions)
debug_print(f"✅ 添加了 {len(gas_transfer_actions)} 个充气动作")
action_sequence.append(create_action_log(f"气体充气协议完成 ({len(gas_transfer_actions)} 个操作)", ""))
else:
debug_print("⚠️ 充气协议返回空序列,添加手动动作")
action_sequence.append(create_action_log("充气协议为空,使用手动等待", "⚠️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": REFILL_TIME}
})
except Exception as e:
debug_print(f"❌ 充气失败: {str(e)}")
# 添加等待时间作为备选
debug_print(f"气体充气失败: {str(e)}")
action_sequence.append(create_action_log(f"气体充气失败: {str(e)}", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": REFILL_TIME}
})
# 充气后等待
refill_wait_minutes = REFILL_TIME / 60
action_sequence.append(create_action_log(f"充气后等待 ({refill_wait_minutes:.1f} 分钟)", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": REFILL_TIME}
@@ -529,7 +672,8 @@ def generate_evacuateandrefill_protocol(
# 关闭气源电磁阀
if gas_solenoid:
debug_print(f"关闭气源电磁阀: {gas_solenoid}")
debug_print(f"🚪 关闭气源电磁阀: {gas_solenoid}")
action_sequence.append(create_action_log(f"关闭气源电磁阀: {gas_solenoid}", "🚪"))
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
@@ -537,68 +681,92 @@ def generate_evacuateandrefill_protocol(
})
# 关闭气源
debug_print(f"关闭气源: {gas_source}")
debug_print(f"🔴 停止气源: {gas_source}")
action_sequence.append(create_action_log(f"停止气源: {gas_source}", "🔴"))
action_sequence.append({
"device_id": gas_source,
"action_name": "set_status",
"action_kwargs": {"string": "OFF"}
})
# 等待下一次循环
# 循环间等待
if cycle < repeats - 1:
debug_print(f"等待下一循环...")
debug_print(f"等待下一循环...")
action_sequence.append(create_action_log("等待下一个循环...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10.0}
})
else:
action_sequence.append(create_action_log(f"{cycle+1}/{repeats} 轮循环完成", ""))
# === 停止搅拌器 ===
debug_print("步骤7: 停止搅拌器...")
debug_print("🔍 步骤7: 停止搅拌器...")
if stirrer_id:
debug_print(f"停止搅拌器: {stirrer_id}")
debug_print(f"🛑 停止搅拌器: {stirrer_id}")
action_sequence.append(create_action_log(f"停止搅拌器: {stirrer_id}", "🛑"))
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {"vessel": vessel}
})
else:
action_sequence.append(create_action_log("跳过搅拌器停止", "⏭️"))
# === 最终等待 ===
action_sequence.append(create_action_log("最终稳定等待...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10.0}
})
# === 总结 ===
total_time = (VACUUM_TIME + REFILL_TIME + 25) * repeats + 20
debug_print("=" * 60)
debug_print(f"抽真空充气协议生成完成")
debug_print(f"总动作数: {len(action_sequence)}")
debug_print(f"处理容器: {vessel}")
debug_print(f"使用气体: {gas}")
debug_print(f"重复次数: {repeats} (硬编码)")
debug_print(f"🎉 抽真空充气协议生成完成")
debug_print(f"📊 协议统计:")
debug_print(f" 📋 总动作数: {len(action_sequence)}")
debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)")
debug_print(f" 🥼 处理容器: {vessel}")
debug_print(f" 💨 使用气体: {gas}")
debug_print(f" 🔄 重复次数: {repeats}")
debug_print("=" * 60)
# 添加完成日志
summary_msg = f"抽真空充气协议完成: {vessel} (使用 {gas}{repeats} 次循环)"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
# === 便捷函数 ===
def generate_nitrogen_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
"""生成氮气置换协议"""
debug_print(f"💨 生成氮气置换协议: {vessel}")
return generate_evacuateandrefill_protocol(G, vessel, "nitrogen", **kwargs)
def generate_argon_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
"""生成氩气置换协议"""
debug_print(f"💨 生成氩气置换协议: {vessel}")
return generate_evacuateandrefill_protocol(G, vessel, "argon", **kwargs)
def generate_air_purge_protocol(G: nx.DiGraph, vessel: str, **kwargs) -> List[Dict[str, Any]]:
"""生成空气置换协议"""
debug_print(f"💨 生成空气置换协议: {vessel}")
return generate_evacuateandrefill_protocol(G, vessel, "air", **kwargs)
def generate_inert_atmosphere_protocol(G: nx.DiGraph, vessel: str, gas: str = "nitrogen", **kwargs) -> List[Dict[str, Any]]:
"""生成惰性气氛协议"""
debug_print(f"🛡️ 生成惰性气氛协议: {vessel} (使用 {gas})")
return generate_evacuateandrefill_protocol(G, vessel, gas, **kwargs)
# 测试函数
def test_evacuateandrefill_protocol():
"""测试抽真空充气协议"""
debug_print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
debug_print("测试完成")
debug_print("=== 抽真空充气协议增强中文版测试 ===")
debug_print("测试完成")
if __name__ == "__main__":
test_evacuateandrefill_protocol()

View File

@@ -273,7 +273,6 @@ def generate_pump_protocol(
if not pump_backbone:
debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀")
# 🔧 对于气体传输,这是正常的,直接返回空序列
return pump_action_sequence
if transfer_flowrate == 0:
@@ -319,10 +318,31 @@ def generate_pump_protocol(
volume_left = volume
debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL")
# 🆕 只在开头打印总体概览
if repeats > 1:
debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL需要 {repeats} 次转移")
logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL需要 {repeats} 次转移")
# 🔧 创建一个自定义的wait动作用于在执行时打印日志
def create_progress_log_action(message: str) -> Dict[str, Any]:
"""创建一个特殊的等待动作,在执行时打印进度日志"""
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1, # 很短的等待时间
"progress_message": message # 自定义字段,用于进度日志
}
}
# 生成泵操作序列
for i in range(repeats):
current_volume = min(volume_left, min_transfer_volume)
# 🆕 在每次循环开始时添加进度日志
if repeats > 1:
start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel}{to_vessel}) 🚰"
pump_action_sequence.append(create_progress_log_action(start_message))
# 🔧 修复:安全地获取边数据
def get_safe_edge_data(node_a, node_b, key):
try:
@@ -426,6 +446,426 @@ def generate_pump_protocol(
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
# 🆕 在每次循环结束时添加完成日志
if repeats > 1:
remaining_volume = volume_left - current_volume
if remaining_volume > 0:
end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳"
else:
end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨"
pump_action_sequence.append(create_progress_log_action(end_message))
volume_left -= current_volume
return pump_action_sequence
def generate_pump_protocol_with_rinsing(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float = 0.0,
amount: str = "",
time: float = 0.0, # 🔧 修复:统一使用 time
viscous: bool = False,
rinsing_solvent: str = "",
rinsing_volume: float = 0.0,
rinsing_repeats: int = 0,
solid: bool = False,
flowrate: float = 2.5,
transfer_flowrate: float = 0.5,
rate_spec: str = "",
event: str = "",
through: str = "",
**kwargs
) -> List[Dict[str, Any]]:
"""
原有的同步版本,添加防冲突机制
"""
# 添加执行锁,防止并发调用
import threading
if not hasattr(generate_pump_protocol_with_rinsing, '_lock'):
generate_pump_protocol_with_rinsing._lock = threading.Lock()
with generate_pump_protocol_with_rinsing._lock:
debug_print("=" * 60)
debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)")
debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
debug_print(f" 🕐 时间戳: {time_module.time()}")
debug_print(f" 🔒 获得执行锁")
debug_print("=" * 60)
# 短暂延迟,避免快速重复调用
time_module.sleep(0.01)
debug_print("🔍 步骤1: 开始体积处理...")
# 1. 处理体积参数
final_volume = volume
debug_print(f"📋 初始设置: final_volume = {final_volume}")
# 🔧 修复如果volume为0ROS2传入的空值从容器读取实际体积
if volume == 0.0:
debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
# 直接从源容器读取实际体积
actual_volume = get_vessel_liquid_volume(G, from_vessel)
debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
if actual_volume > 0:
final_volume = actual_volume
debug_print(f"✅ 成功设置体积为: {final_volume}mL")
else:
final_volume = 10.0 # 如果读取失败,使用默认值
logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
else:
debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
# 处理 amount 参数
if amount and amount.strip():
debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
parsed_volume = _parse_amount_to_volume(amount)
debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
if parsed_volume > 0:
final_volume = parsed_volume
debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
elif parsed_volume == 0.0 and amount.lower().strip() == "all":
debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
actual_volume = get_vessel_liquid_volume(G, from_vessel)
if actual_volume > 0:
final_volume = actual_volume
debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
# 最终体积验证
debug_print(f"🔍 步骤2: 最终体积验证...")
if final_volume <= 0:
logger.error(f"❌ 体积无效: {final_volume}mL")
final_volume = 10.0
logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL")
debug_print(f"✅ 最终确定体积: {final_volume}mL")
# 2. 处理流速参数
debug_print(f"🔍 步骤3: 处理流速参数...")
debug_print(f" - 原始 flowrate: {flowrate}")
debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
final_flowrate = flowrate if flowrate > 0 else 2.5
final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
if flowrate <= 0:
logger.warning(f"⚠️ flowrate <= 0修正为: {final_flowrate}mL/s")
if transfer_flowrate <= 0:
logger.warning(f"⚠️ transfer_flowrate <= 0修正为: {final_transfer_flowrate}mL/s")
debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
# 3. 根据时间计算流速
if time > 0 and final_volume > 0:
debug_print(f"🔍 步骤4: 根据时间计算流速...")
calculated_flowrate = final_volume / time
debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
if flowrate <= 0 or flowrate == 2.5:
final_flowrate = min(calculated_flowrate, 10.0)
debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
final_transfer_flowrate = min(calculated_flowrate, 5.0)
debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
# 4. 根据速度规格调整
if rate_spec:
debug_print(f"🔍 步骤5: 根据速度规格调整...")
debug_print(f" - 速度规格: '{rate_spec}'")
if rate_spec == "dropwise":
final_flowrate = min(final_flowrate, 0.1)
final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
debug_print(f" - dropwise模式流速调整为: {final_flowrate}mL/s")
elif rate_spec == "slowly":
final_flowrate = min(final_flowrate, 0.5)
final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
debug_print(f" - slowly模式流速调整为: {final_flowrate}mL/s")
elif rate_spec == "quickly":
final_flowrate = max(final_flowrate, 5.0)
final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
debug_print(f" - quickly模式流速调整为: {final_flowrate}mL/s")
try:
# 🆕 修复在这里调用带有循环日志的generate_pump_protocol_with_loop_logging函数
pump_action_sequence = generate_pump_protocol_with_loop_logging(
G, from_vessel, to_vessel, final_volume,
final_flowrate, final_transfer_flowrate
)
debug_print(f"🔓 释放执行锁")
return pump_action_sequence
except Exception as e:
logger.error(f"❌ 协议生成失败: {str(e)}")
return [
{
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"❌ 协议生成失败: {str(e)}"
}
}
]
def generate_pump_protocol_with_loop_logging(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
flowrate: float = 2.5,
transfer_flowrate: float = 0.5,
) -> List[Dict[str, Any]]:
"""
生成泵操作的动作序列 - 带循环日志版本
🔧 修复:正确处理包含电磁阀的路径,并在合适时机打印循环日志
"""
pump_action_sequence = []
nodes = G.nodes(data=True)
# 验证输入参数
if volume <= 0:
logger.error(f"无效的体积参数: {volume}mL")
return pump_action_sequence
if flowrate <= 0:
flowrate = 2.5
logger.warning(f"flowrate <= 0使用默认值 {flowrate}mL/s")
if transfer_flowrate <= 0:
transfer_flowrate = 0.5
logger.warning(f"transfer_flowrate <= 0使用默认值 {transfer_flowrate}mL/s")
# 验证容器存在
if from_vessel not in G.nodes():
logger.error(f"源容器 '{from_vessel}' 不存在")
return pump_action_sequence
if to_vessel not in G.nodes():
logger.error(f"目标容器 '{to_vessel}' 不存在")
return pump_action_sequence
try:
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
debug_print(f"PUMP_TRANSFER: 路径 {from_vessel} -> {to_vessel}: {shortest_path}")
except nx.NetworkXNoPath:
logger.error(f"无法找到从 '{from_vessel}''{to_vessel}' 的路径")
return pump_action_sequence
# 🔧 关键修复:正确构建泵骨架,排除容器和电磁阀
pump_backbone = []
for node in shortest_path:
# 跳过起始和结束容器
if node == from_vessel or node == to_vessel:
continue
# 跳过电磁阀(电磁阀不参与泵操作)
node_data = G.nodes.get(node, {})
node_class = node_data.get("class", "") or ""
if ("solenoid" in node_class.lower() or "solenoid_valve" in node.lower()):
debug_print(f"PUMP_TRANSFER: 跳过电磁阀 {node}")
continue
# 只包含多通阀和泵
if ("multiway" in node_class.lower() or "valve" in node_class.lower() or "pump" in node_class.lower()):
pump_backbone.append(node)
debug_print(f"PUMP_TRANSFER: 过滤后的泵骨架: {pump_backbone}")
if not pump_backbone:
debug_print("PUMP_TRANSFER: 没有泵骨架节点,可能是直接容器连接或只有电磁阀")
return pump_action_sequence
if transfer_flowrate == 0:
transfer_flowrate = flowrate
try:
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
except Exception as e:
debug_print(f"PUMP_TRANSFER: 构建泵-阀门映射失败: {str(e)}")
return pump_action_sequence
if not pumps_from_node:
debug_print("PUMP_TRANSFER: 没有可用的泵映射")
return pump_action_sequence
# 🔧 修复:安全地获取最小转移体积
try:
min_transfer_volumes = []
for node in pump_backbone:
if node in pumps_from_node:
pump_node = pumps_from_node[node]
if pump_node in nodes:
pump_config = nodes[pump_node].get("config", {})
max_volume = pump_config.get("max_volume")
if max_volume is not None:
min_transfer_volumes.append(max_volume)
if min_transfer_volumes:
min_transfer_volume = min(min_transfer_volumes)
else:
min_transfer_volume = 25.0 # 默认值
debug_print(f"PUMP_TRANSFER: 无法获取泵的最大体积,使用默认值: {min_transfer_volume}mL")
except Exception as e:
debug_print(f"PUMP_TRANSFER: 获取最小转移体积失败: {str(e)}")
min_transfer_volume = 25.0 # 默认值
repeats = int(np.ceil(volume / min_transfer_volume))
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
logger.error("Cannot transfer volume larger than min_transfer_volume between two pumps.")
return pump_action_sequence
volume_left = volume
debug_print(f"PUMP_TRANSFER: 需要 {repeats} 次转移,单次最大体积 {min_transfer_volume} mL")
# 🆕 只在开头打印总体概览
if repeats > 1:
debug_print(f"🔄 分批转移概览: 总体积 {volume:.2f}mL需要 {repeats} 次转移")
logger.info(f"🔄 分批转移概览: 总体积 {volume:.2f}mL需要 {repeats} 次转移")
# 🔧 创建一个自定义的wait动作用于在执行时打印日志
def create_progress_log_action(message: str) -> Dict[str, Any]:
"""创建一个特殊的等待动作,在执行时打印进度日志"""
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1, # 很短的等待时间
"progress_message": message # 自定义字段,用于进度日志
}
}
# 生成泵操作序列
for i in range(repeats):
current_volume = min(volume_left, min_transfer_volume)
# 🆕 在每次循环开始时添加进度日志
if repeats > 1:
start_message = f"🚀 准备开始第 {i+1}/{repeats} 次转移: {current_volume:.2f}mL ({from_vessel}{to_vessel}) 🚰"
pump_action_sequence.append(create_progress_log_action(start_message))
# 🔧 修复:安全地获取边数据
def get_safe_edge_data(node_a, node_b, key):
try:
edge_data = G.get_edge_data(node_a, node_b)
if edge_data and "port" in edge_data:
port_data = edge_data["port"]
if isinstance(port_data, dict) and key in port_data:
return port_data[key]
return "default"
except Exception as e:
debug_print(f"PUMP_TRANSFER: 获取边数据失败 {node_a}->{node_b}: {str(e)}")
return "default"
# 从源容器吸液
if not from_vessel.startswith("pump") and pump_backbone:
first_pump_node = pump_backbone[0]
if first_pump_node in valve_from_node and first_pump_node in pumps_from_node:
port_command = get_safe_edge_data(first_pump_node, from_vessel, first_pump_node)
pump_action_sequence.extend([
{
"device_id": valve_from_node[first_pump_node],
"action_name": "set_valve_position",
"action_kwargs": {
"command": port_command
}
},
{
"device_id": pumps_from_node[first_pump_node],
"action_name": "set_position",
"action_kwargs": {
"position": float(current_volume),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
# 泵间转移
for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
if nodeA in valve_from_node and nodeB in valve_from_node and nodeA in pumps_from_node and nodeB in pumps_from_node:
port_a = get_safe_edge_data(nodeA, nodeB, nodeA)
port_b = get_safe_edge_data(nodeB, nodeA, nodeB)
pump_action_sequence.append([
{
"device_id": valve_from_node[nodeA],
"action_name": "set_valve_position",
"action_kwargs": {
"command": port_a
}
},
{
"device_id": valve_from_node[nodeB],
"action_name": "set_valve_position",
"action_kwargs": {
"command": port_b
}
}
])
pump_action_sequence.append([
{
"device_id": pumps_from_node[nodeA],
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": transfer_flowrate
}
},
{
"device_id": pumps_from_node[nodeB],
"action_name": "set_position",
"action_kwargs": {
"position": float(current_volume),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
# 排液到目标容器
if not to_vessel.startswith("pump") and pump_backbone:
last_pump_node = pump_backbone[-1]
if last_pump_node in valve_from_node and last_pump_node in pumps_from_node:
port_command = get_safe_edge_data(last_pump_node, to_vessel, last_pump_node)
pump_action_sequence.extend([
{
"device_id": valve_from_node[last_pump_node],
"action_name": "set_valve_position",
"action_kwargs": {
"command": port_command
}
},
{
"device_id": pumps_from_node[last_pump_node],
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 3}})
# 🆕 在每次循环结束时添加完成日志
if repeats > 1:
remaining_volume = volume_left - current_volume
if remaining_volume > 0:
end_message = f"✅ 第 {i+1}/{repeats} 次转移完成! 剩余 {remaining_volume:.2f}mL 待转移 ⏳"
else:
end_message = f"🎉 第 {i+1}/{repeats} 次转移完成! 全部 {volume:.2f}mL 转移完毕 ✨"
pump_action_sequence.append(create_progress_log_action(end_message))
volume_left -= current_volume
return pump_action_sequence
@@ -892,58 +1332,386 @@ def generate_pump_protocol_with_rinsing(
final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
debug_print(f" - quickly模式流速调整为: {final_flowrate}mL/s")
# # 5. 处理冲洗参数
# debug_print(f"🔍 步骤6: 处理冲洗参数...")
# final_rinsing_solvent = rinsing_solvent
# final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
# final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
# # 5. 处理冲洗参数
# debug_print(f"🔍 步骤6: 处理冲洗参数...")
# final_rinsing_solvent = rinsing_solvent
# final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
# final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
# if rinsing_volume <= 0:
# logger.warning(f"⚠️ rinsing_volume <= 0修正为: {final_rinsing_volume}mL")
# if rinsing_repeats <= 0:
# logger.warning(f"⚠️ rinsing_repeats <= 0修正为: {final_rinsing_repeats}次")
# if rinsing_volume <= 0:
# logger.warning(f"⚠️ rinsing_volume <= 0修正为: {final_rinsing_volume}mL")
# if rinsing_repeats <= 0:
# logger.warning(f"⚠️ rinsing_repeats <= 0修正为: {final_rinsing_repeats}次")
# # 根据物理属性调整冲洗参数
# if viscous or solid:
# final_rinsing_repeats = max(final_rinsing_repeats, 3)
# final_rinsing_volume = max(final_rinsing_volume, 10.0)
# debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
# # 根据物理属性调整冲洗参数
# if viscous or solid:
# final_rinsing_repeats = max(final_rinsing_repeats, 3)
# final_rinsing_volume = max(final_rinsing_volume, 10.0)
# debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
try:
pump_action_sequence = generate_pump_protocol(
G, from_vessel, to_vessel, final_volume,
flowrate, transfer_flowrate
)
# 参数总结
debug_print("📊 最终参数总结:")
debug_print(f" - 体积: {final_volume}mL")
debug_print(f" - 流速: {final_flowrate}mL/s")
debug_print(f" - 转移流速: {final_transfer_flowrate}mL/s")
# debug_print(f" - 冲洗溶剂: '{final_rinsing_solvent}'")
# debug_print(f" - 冲洗体积: {final_rinsing_volume}mL")
# debug_print(f" - 冲洗次数: {final_rinsing_repeats}次")
# 为每个动作添加唯一标识
# for i, action in enumerate(pump_action_sequence):
# if isinstance(action, dict):
# action['_protocol_id'] = protocol_id
# action['_action_sequence'] = i
# elif isinstance(action, list):
# for j, sub_action in enumerate(action):
# if isinstance(sub_action, dict):
# sub_action['_protocol_id'] = protocol_id
# sub_action['_action_sequence'] = f"{i}_{j}"
#
# debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作")
debug_print(f"🔓 释放执行锁")
return pump_action_sequence
# ========== 执行基础转移 ==========
debug_print("🔧 步骤7: 开始执行基础转移...")
try:
debug_print(f" - 调用 generate_pump_protocol...")
debug_print(f" - 参数: G, '{from_vessel}', '{to_vessel}', {final_volume}, {final_flowrate}, {final_transfer_flowrate}")
pump_action_sequence = generate_pump_protocol(
G, from_vessel, to_vessel, final_volume,
final_flowrate, final_transfer_flowrate
)
debug_print(f" - generate_pump_protocol 返回结果:")
debug_print(f" - 动作序列长度: {len(pump_action_sequence)}")
debug_print(f" - 动作序列是否为空: {len(pump_action_sequence) == 0}")
if not pump_action_sequence:
debug_print("❌ 基础转移协议生成为空,可能是路径问题")
debug_print(f" - 源容器存在: {from_vessel in G.nodes()}")
debug_print(f" - 目标容器存在: {to_vessel in G.nodes()}")
if from_vessel in G.nodes() and to_vessel in G.nodes():
try:
path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
debug_print(f" - 路径存在: {path}")
except Exception as path_error:
debug_print(f" - 无法找到路径: {str(path_error)}")
except Exception as e:
logger.error(f"❌ 协议生成失败: {str(e)}")
return [
{
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"❌ 协议生成失败: {str(e)}"
},
'_protocol_id': protocol_id,
'_action_sequence': 0
"message": f"⚠️ 路径问题,无法转移: {final_volume}mL 从 {from_vessel}{to_vessel}"
}
}
]
debug_print(f"✅ 基础转移生成了 {len(pump_action_sequence)} 个动作")
# 打印前几个动作用于调试
if len(pump_action_sequence) > 0:
debug_print("🔍 前几个动作预览:")
for i, action in enumerate(pump_action_sequence[:3]):
debug_print(f" 动作 {i+1}: {action}")
if len(pump_action_sequence) > 3:
debug_print(f" ... 还有 {len(pump_action_sequence) - 3} 个动作")
except Exception as e:
debug_print(f"❌ 基础转移失败: {str(e)}")
import traceback
debug_print(f"详细错误: {traceback.format_exc()}")
return [
{
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"❌ 转移失败: {final_volume}mL 从 {from_vessel}{to_vessel}, 错误: {str(e)}"
}
}
]
# ========== 执行冲洗操作 ==========
# debug_print("🔧 步骤8: 检查冲洗操作...")
# if final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0:
# debug_print(f"🧽 开始冲洗操作,溶剂: '{final_rinsing_solvent}'")
# try:
# if final_rinsing_solvent.strip() != "air":
# debug_print(" - 执行液体冲洗...")
# rinsing_actions = _generate_rinsing_sequence(
# G, from_vessel, to_vessel, final_rinsing_solvent,
# final_rinsing_volume, final_rinsing_repeats,
# final_flowrate, final_transfer_flowrate
# )
# pump_action_sequence.extend(rinsing_actions)
# debug_print(f" - 添加了 {len(rinsing_actions)} 个冲洗动作")
# else:
# debug_print(" - 执行空气冲洗...")
# air_rinsing_actions = _generate_air_rinsing_sequence(
# G, from_vessel, to_vessel, final_rinsing_volume, final_rinsing_repeats,
# final_flowrate, final_transfer_flowrate
# )
# pump_action_sequence.extend(air_rinsing_actions)
# debug_print(f" - 添加了 {len(air_rinsing_actions)} 个空气冲洗动作")
# except Exception as e:
# debug_print(f"⚠️ 冲洗操作失败: {str(e)},跳过冲洗")
# else:
# debug_print(f"⏭️ 跳过冲洗操作")
# debug_print(f" - 溶剂: '{final_rinsing_solvent}'")
# debug_print(f" - 次数: {final_rinsing_repeats}")
# debug_print(f" - 条件满足: {bool(final_rinsing_solvent and final_rinsing_solvent.strip() and final_rinsing_repeats > 0)}")
# ========== 最终结果 ==========
debug_print("=" * 60)
debug_print(f"🎉 PUMP_TRANSFER: 协议生成完成")
debug_print(f" 📊 总动作数: {len(pump_action_sequence)}")
debug_print(f" 📋 最终体积: {final_volume}mL")
debug_print(f" 🚀 执行路径: {from_vessel} -> {to_vessel}")
# 最终验证
if len(pump_action_sequence) == 0:
debug_print("🚨 协议生成结果为空!这是异常情况")
return [
{
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"🚨 协议生成失败: 无法生成任何动作序列"
}
}
]
debug_print("=" * 60)
return pump_action_sequence
async def generate_pump_protocol_with_rinsing_async(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float = 0.0,
amount: str = "",
time: float = 0.0,
viscous: bool = False,
rinsing_solvent: str = "",
rinsing_volume: float = 0.0,
rinsing_repeats: int = 0,
solid: bool = False,
flowrate: float = 2.5,
transfer_flowrate: float = 0.5,
rate_spec: str = "",
event: str = "",
through: str = "",
**kwargs
) -> List[Dict[str, Any]]:
"""
异步版本的泵转移协议生成器,避免并发问题
"""
debug_print("=" * 60)
debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (异步版本)")
debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
debug_print(f" 🕐 时间戳: {time_module.time()}")
debug_print("=" * 60)
# 添加唯一标识符
protocol_id = f"pump_transfer_{int(time_module.time() * 1000000)}"
debug_print(f"📋 协议ID: {protocol_id}")
# 调用原有的同步版本
result = generate_pump_protocol_with_rinsing(
G, from_vessel, to_vessel, volume, amount, time, viscous,
rinsing_solvent, rinsing_volume, rinsing_repeats, solid,
flowrate, transfer_flowrate, rate_spec, event, through, **kwargs
)
# 为每个动作添加唯一标识
for i, action in enumerate(result):
if isinstance(action, dict):
action['_protocol_id'] = protocol_id
action['_action_sequence'] = i
action['_timestamp'] = time_module.time()
debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(result)} 个动作")
return result
# 保持原有的同步版本兼容性
def generate_pump_protocol_with_rinsing(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float = 0.0,
amount: str = "",
time: float = 0.0,
viscous: bool = False,
rinsing_solvent: str = "",
rinsing_volume: float = 0.0,
rinsing_repeats: int = 0,
solid: bool = False,
flowrate: float = 2.5,
transfer_flowrate: float = 0.5,
rate_spec: str = "",
event: str = "",
through: str = "",
**kwargs
) -> List[Dict[str, Any]]:
"""
原有的同步版本,添加防冲突机制
"""
# 添加执行锁,防止并发调用
import threading
if not hasattr(generate_pump_protocol_with_rinsing, '_lock'):
generate_pump_protocol_with_rinsing._lock = threading.Lock()
with generate_pump_protocol_with_rinsing._lock:
debug_print("=" * 60)
debug_print(f"PUMP_TRANSFER: 🚀 开始生成协议 (同步版本)")
debug_print(f" 📍 路径: {from_vessel} -> {to_vessel}")
debug_print(f" 🕐 时间戳: {time_module.time()}")
debug_print(f" 🔒 获得执行锁")
debug_print("=" * 60)
# 短暂延迟,避免快速重复调用
time_module.sleep(0.01)
debug_print("🔍 步骤1: 开始体积处理...")
# 1. 处理体积参数
final_volume = volume
debug_print(f"📋 初始设置: final_volume = {final_volume}")
# 🔧 修复如果volume为0ROS2传入的空值从容器读取实际体积
if volume == 0.0:
debug_print("🎯 检测到 volume=0.0,开始自动体积检测...")
# 直接从源容器读取实际体积
actual_volume = get_vessel_liquid_volume(G, from_vessel)
debug_print(f"📖 从容器 '{from_vessel}' 读取到体积: {actual_volume}mL")
if actual_volume > 0:
final_volume = actual_volume
debug_print(f"✅ 成功设置体积为: {final_volume}mL")
else:
final_volume = 10.0 # 如果读取失败,使用默认值
logger.warning(f"⚠️ 无法从容器读取体积,使用默认值: {final_volume}mL")
else:
debug_print(f"📌 体积非零,直接使用: {final_volume}mL")
# 处理 amount 参数
if amount and amount.strip():
debug_print(f"🔍 检测到 amount 参数: '{amount}',开始解析...")
parsed_volume = _parse_amount_to_volume(amount)
debug_print(f"📖 从 amount 解析得到体积: {parsed_volume}mL")
if parsed_volume > 0:
final_volume = parsed_volume
debug_print(f"✅ 使用从 amount 解析的体积: {final_volume}mL")
elif parsed_volume == 0.0 and amount.lower().strip() == "all":
debug_print("🎯 检测到 amount='all',从容器读取全部体积...")
actual_volume = get_vessel_liquid_volume(G, from_vessel)
if actual_volume > 0:
final_volume = actual_volume
debug_print(f"✅ amount='all',设置体积为: {final_volume}mL")
# 最终体积验证
debug_print(f"🔍 步骤2: 最终体积验证...")
if final_volume <= 0:
logger.error(f"❌ 体积无效: {final_volume}mL")
final_volume = 10.0
logger.warning(f"⚠️ 强制设置为默认值: {final_volume}mL")
debug_print(f"✅ 最终确定体积: {final_volume}mL")
# 2. 处理流速参数
debug_print(f"🔍 步骤3: 处理流速参数...")
debug_print(f" - 原始 flowrate: {flowrate}")
debug_print(f" - 原始 transfer_flowrate: {transfer_flowrate}")
final_flowrate = flowrate if flowrate > 0 else 2.5
final_transfer_flowrate = transfer_flowrate if transfer_flowrate > 0 else 0.5
if flowrate <= 0:
logger.warning(f"⚠️ flowrate <= 0修正为: {final_flowrate}mL/s")
if transfer_flowrate <= 0:
logger.warning(f"⚠️ transfer_flowrate <= 0修正为: {final_transfer_flowrate}mL/s")
debug_print(f"✅ 修正后流速: flowrate={final_flowrate}mL/s, transfer_flowrate={final_transfer_flowrate}mL/s")
# 3. 根据时间计算流速
if time > 0 and final_volume > 0:
debug_print(f"🔍 步骤4: 根据时间计算流速...")
calculated_flowrate = final_volume / time
debug_print(f" - 计算得到流速: {calculated_flowrate}mL/s")
if flowrate <= 0 or flowrate == 2.5:
final_flowrate = min(calculated_flowrate, 10.0)
debug_print(f" - 调整 flowrate 为: {final_flowrate}mL/s")
if transfer_flowrate <= 0 or transfer_flowrate == 0.5:
final_transfer_flowrate = min(calculated_flowrate, 5.0)
debug_print(f" - 调整 transfer_flowrate 为: {final_transfer_flowrate}mL/s")
# 4. 根据速度规格调整
if rate_spec:
debug_print(f"🔍 步骤5: 根据速度规格调整...")
debug_print(f" - 速度规格: '{rate_spec}'")
if rate_spec == "dropwise":
final_flowrate = min(final_flowrate, 0.1)
final_transfer_flowrate = min(final_transfer_flowrate, 0.1)
debug_print(f" - dropwise模式流速调整为: {final_flowrate}mL/s")
elif rate_spec == "slowly":
final_flowrate = min(final_flowrate, 0.5)
final_transfer_flowrate = min(final_transfer_flowrate, 0.3)
debug_print(f" - slowly模式流速调整为: {final_flowrate}mL/s")
elif rate_spec == "quickly":
final_flowrate = max(final_flowrate, 5.0)
final_transfer_flowrate = max(final_transfer_flowrate, 2.0)
debug_print(f" - quickly模式流速调整为: {final_flowrate}mL/s")
# # 5. 处理冲洗参数
# debug_print(f"🔍 步骤6: 处理冲洗参数...")
# final_rinsing_solvent = rinsing_solvent
# final_rinsing_volume = rinsing_volume if rinsing_volume > 0 else 5.0
# final_rinsing_repeats = rinsing_repeats if rinsing_repeats > 0 else 2
# if rinsing_volume <= 0:
# logger.warning(f"⚠️ rinsing_volume <= 0修正为: {final_rinsing_volume}mL")
# if rinsing_repeats <= 0:
# logger.warning(f"⚠️ rinsing_repeats <= 0修正为: {final_rinsing_repeats}次")
# # 根据物理属性调整冲洗参数
# if viscous or solid:
# final_rinsing_repeats = max(final_rinsing_repeats, 3)
# final_rinsing_volume = max(final_rinsing_volume, 10.0)
# debug_print(f"🧪 粘稠/固体物质,调整冲洗参数:{final_rinsing_repeats}次,{final_rinsing_volume}mL")
try:
pump_action_sequence = generate_pump_protocol(
G, from_vessel, to_vessel, final_volume,
flowrate, transfer_flowrate
)
# 为每个动作添加唯一标识
# for i, action in enumerate(pump_action_sequence):
# if isinstance(action, dict):
# action['_protocol_id'] = protocol_id
# action['_action_sequence'] = i
# elif isinstance(action, list):
# for j, sub_action in enumerate(action):
# if isinstance(sub_action, dict):
# sub_action['_protocol_id'] = protocol_id
# sub_action['_action_sequence'] = f"{i}_{j}"
#
# debug_print(f"📊 协议 {protocol_id} 生成完成,共 {len(pump_action_sequence)} 个动作")
debug_print(f"🔓 释放执行锁")
return pump_action_sequence
except Exception as e:
logger.error(f"❌ 协议生成失败: {str(e)}")
return [
{
"device_id": "system",
"action_name": "log_message",
"action_kwargs": {
"message": f"❌ 协议生成失败: {str(e)}"
}
}
]
def _parse_amount_to_volume(amount: str) -> float:
"""解析 amount 字符串为体积"""
debug_print(f"🔍 解析 amount: '{amount}'")

View File

@@ -1,15 +1,67 @@
import networkx as nx
import re
import logging
import sys
from typing import List, Dict, Any, Union
from .pump_protocol import generate_pump_protocol_with_rinsing
logger = logging.getLogger(__name__)
# 确保输出编码为UTF-8
if hasattr(sys.stdout, 'reconfigure'):
try:
sys.stdout.reconfigure(encoding='utf-8')
sys.stderr.reconfigure(encoding='utf-8')
except:
pass
def debug_print(message):
"""调试输出"""
print(f"[SEPARATE] {message}", flush=True)
logger.info(f"[SEPARATE] {message}")
"""调试输出函数 - 支持中文"""
try:
# 确保消息是字符串格式
safe_message = str(message)
print(f"[分离协议] {safe_message}", flush=True)
logger.info(f"[分离协议] {safe_message}")
except UnicodeEncodeError:
# 如果编码失败,尝试替换不支持的字符
safe_message = str(message).encode('utf-8', errors='replace').decode('utf-8')
print(f"[分离协议] {safe_message}", flush=True)
logger.info(f"[分离协议] {safe_message}")
except Exception as e:
# 最后的安全措施
fallback_message = f"日志输出错误: {repr(message)}"
print(f"[分离协议] {fallback_message}", flush=True)
logger.info(f"[分离协议] {fallback_message}")
def create_action_log(message: str, emoji: str = "📝") -> Dict[str, Any]:
"""创建一个动作日志 - 支持中文和emoji"""
try:
full_message = f"{emoji} {message}"
debug_print(full_message)
logger.info(full_message)
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": full_message,
"progress_message": full_message
}
}
except Exception as e:
# 如果emoji有问题使用纯文本
safe_message = f"[日志] {message}"
debug_print(safe_message)
logger.info(safe_message)
return {
"action_name": "wait",
"action_kwargs": {
"time": 0.1,
"log_message": safe_message,
"progress_message": safe_message
}
}
def parse_volume_input(volume_input: Union[str, float]) -> float:
"""
@@ -22,52 +74,58 @@ def parse_volume_input(volume_input: Union[str, float]) -> float:
float: 体积(毫升)
"""
if isinstance(volume_input, (int, float)):
debug_print(f"📏 体积输入为数值: {volume_input}")
return float(volume_input)
if not volume_input or not str(volume_input).strip():
debug_print(f"⚠️ 体积输入为空,返回 0.0mL")
return 0.0
volume_str = str(volume_input).lower().strip()
debug_print(f"解析体积输入: '{volume_str}'")
debug_print(f"🔍 解析体积输入: '{volume_str}'")
# 处理未知体积
if volume_str in ['?', 'unknown', 'tbd', 'to be determined']:
if volume_str in ['?', 'unknown', 'tbd', 'to be determined', '未知', '待定']:
default_volume = 100.0 # 默认100mL
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
debug_print(f"检测到未知体积,使用默认值: {default_volume}mL")
return default_volume
# 移除空格并提取数字和单位
volume_clean = re.sub(r'\s+', '', volume_str)
# 匹配数字和单位的正则表达式
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter)?', volume_clean)
match = re.match(r'([0-9]*\.?[0-9]+)\s*(ml|l|μl|ul|microliter|milliliter|liter|毫升|升|微升)?', volume_clean)
if not match:
debug_print(f"⚠️ 无法解析体积: '{volume_str}'使用默认值100mL")
debug_print(f"⚠️ 无法解析体积: '{volume_str}',使用默认值 100mL")
return 100.0
value = float(match.group(1))
unit = match.group(2) or 'ml' # 默认单位为毫升
# 转换为毫升
if unit in ['l', 'liter']:
if unit in ['l', 'liter', '']:
volume = value * 1000.0 # L -> mL
elif unit in ['μl', 'ul', 'microliter']:
debug_print(f"🔄 体积转换: {value}L -> {volume}mL")
elif unit in ['μl', 'ul', 'microliter', '微升']:
volume = value / 1000.0 # μL -> mL
else: # ml, milliliter 或默认
debug_print(f"🔄 体积转换: {value}μL -> {volume}mL")
else: # ml, milliliter, 毫升 或默认
volume = value # 已经是mL
debug_print(f"✅ 体积已为毫升单位: {volume}mL")
debug_print(f"体积转换: {value}{unit}{volume}mL")
return volume
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""查找溶剂容器"""
"""查找溶剂容器,支持多种匹配模式"""
if not solvent or not solvent.strip():
debug_print("⏭️ 未指定溶剂,跳过溶剂容器查找")
return ""
debug_print(f"查找溶剂 '{solvent}' 的容器...")
debug_print(f"🔍 正在查找溶剂 '{solvent}' 的容器...")
# 🔧 方法1直接搜索 data.reagent_name 和 config.reagent
debug_print(f"📋 方法1: 搜索试剂字段...")
for node in G.nodes():
node_data = G.nodes[node].get('data', {})
node_type = G.nodes[node].get('type', '')
@@ -80,16 +138,17 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
# 精确匹配
if reagent_name == solvent.lower() or config_reagent == solvent.lower():
debug_print(f"✅ 通过reagent字段找到容器: {node}")
debug_print(f"✅ 通过试剂字段精确匹配找到容器: {node}")
return node
# 模糊匹配
if (solvent.lower() in reagent_name and reagent_name) or \
(solvent.lower() in config_reagent and config_reagent):
debug_print(f"✅ 通过reagent字段模糊匹配到容器: {node}")
debug_print(f"✅ 通过试剂字段模糊匹配到容器: {node}")
return node
# 🔧 方法2常见的容器命名规则
debug_print(f"📋 方法2: 使用命名规则...")
solvent_clean = solvent.lower().replace(' ', '_').replace('-', '_')
possible_names = [
f"flask_{solvent_clean}",
@@ -99,9 +158,14 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
f"{solvent_clean}_bottle",
f"solvent_{solvent_clean}",
f"reagent_{solvent_clean}",
f"reagent_bottle_{solvent_clean}"
f"reagent_bottle_{solvent_clean}",
f"reagent_bottle_1", # 通用试剂瓶
f"reagent_bottle_2",
f"reagent_bottle_3"
]
debug_print(f"🎯 尝试的容器名称: {possible_names[:5]}... (共 {len(possible_names)} 个)")
for name in possible_names:
if name in G.nodes():
node_type = G.nodes[name].get('type', '')
@@ -110,53 +174,94 @@ def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
return name
# 🔧 方法3使用第一个试剂瓶作为备选
debug_print(f"📋 方法3: 查找备用试剂瓶...")
for node_id in G.nodes():
node_data = G.nodes[node_id]
if (node_data.get('type') == 'container' and
('reagent' in node_id.lower() or 'bottle' in node_id.lower())):
debug_print(f"⚠️ 未找到专用容器,使用备容器: {node_id}")
debug_print(f"⚠️ 未找到专用容器,使用备容器: {node_id}")
return node_id
debug_print(f"⚠️ 未找到溶剂 '{solvent}' 的容器")
debug_print(f"❌ 无法找到溶剂 '{solvent}' 的容器")
return ""
def find_separator_device(G: nx.DiGraph, vessel: str) -> str:
"""查找分离器设备"""
debug_print(f"查找容器 '{vessel}' 对应的分离器设备...")
"""查找分离器设备,支持多种查找方式"""
debug_print(f"🔍 正在查找容器 '{vessel}' 的分离器设备...")
# 方法1查找连接到容器的分离器设备
debug_print(f"📋 方法1: 检查连接的分离器...")
separator_nodes = []
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'separator' in node_class:
separator_nodes.append(node)
debug_print(f"📋 发现分离器设备: {node}")
# 检查是否连接到目标容器
if G.has_edge(node, vessel) or G.has_edge(vessel, node):
debug_print(f"✅ 找到连接的分离器: {node}")
return node
debug_print(f"📊 找到的分离器总数: {len(separator_nodes)}")
# 方法2根据命名规则查找
debug_print(f"📋 方法2: 使用命名规则...")
possible_names = [
f"{vessel}_controller",
f"{vessel}_separator",
vessel, # 容器本身可能就是分离器
"separator_1",
"virtual_separator"
"virtual_separator",
"liquid_handler_1", # 液体处理器也可能用于分离
"controller_1"
]
debug_print(f"🎯 尝试的分离器名称: {possible_names}")
for name in possible_names:
if name in G.nodes():
node_class = G.nodes[name].get('class', '').lower()
if 'separator' in node_class:
if 'separator' in node_class or 'controller' in node_class:
debug_print(f"✅ 通过命名规则找到分离器: {name}")
return name
# 方法3查找第一个分离器设备
for node in G.nodes():
node_class = G.nodes[node].get('class', '').lower()
if 'separator' in node_class:
debug_print(f"⚠️ 使用第一个分离器设备: {node}")
return node
debug_print(f"📋 方法3: 使用第一个可用分离器...")
if separator_nodes:
debug_print(f"⚠️ 使用第一个分离器设备: {separator_nodes[0]}")
return separator_nodes[0]
debug_print(f"⚠️ 未找到分离器设备")
debug_print(f" 未找到分离器设备")
return ""
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找连接到指定容器的搅拌器"""
debug_print(f"🔍 正在查找与容器 {vessel} 连接的搅拌器...")
stirrer_nodes = []
for node in G.nodes():
node_data = G.nodes[node]
node_class = node_data.get('class', '') or ''
if 'stirrer' in node_class.lower():
stirrer_nodes.append(node)
debug_print(f"📋 发现搅拌器: {node}")
debug_print(f"📊 找到的搅拌器总数: {len(stirrer_nodes)}")
# 检查哪个搅拌器与目标容器相连
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
debug_print(f"✅ 找到连接的搅拌器: {stirrer}")
return stirrer
# 如果没有连接的搅拌器,返回第一个可用的
if stirrer_nodes:
debug_print(f"⚠️ 未找到直接连接的搅拌器,使用第一个可用的: {stirrer_nodes[0]}")
return stirrer_nodes[0]
debug_print("❌ 未找到搅拌器")
return ""
def generate_separate_protocol(
@@ -185,7 +290,7 @@ def generate_separate_protocol(
**kwargs
) -> List[Dict[str, Any]]:
"""
生成分离操作的协议序列 - 修复
生成分离操作的协议序列 - 增强中文
支持XDL参数格式
- vessel: 分离容器(必需)
@@ -206,26 +311,31 @@ def generate_separate_protocol(
"""
debug_print("=" * 60)
debug_print("开始生成分离协议 - 修复")
debug_print(f"原始参数:")
debug_print(f" - vessel: '{vessel}'")
debug_print(f" - purpose: '{purpose}'")
debug_print(f" - product_phase: '{product_phase}'")
debug_print(f" - solvent: '{solvent}'")
debug_print(f" - volume: {volume} (类型: {type(volume)})")
debug_print(f" - repeats: {repeats}")
debug_print(f" - product_vessel: '{product_vessel}'")
debug_print(f" - waste_vessel: '{waste_vessel}'")
debug_print("🧪 开始生成分离协议 - 增强中文")
debug_print(f"📋 原始参数:")
debug_print(f" 🥼 容器: '{vessel}'")
debug_print(f" 🎯 分离目的: '{purpose}'")
debug_print(f" 📊 产物相: '{product_phase}'")
debug_print(f" 💧 溶剂: '{solvent}'")
debug_print(f" 📏 体积: {volume} (类型: {type(volume)})")
debug_print(f" 🔄 重复次数: {repeats}")
debug_print(f" 🎯 产物容器: '{product_vessel}'")
debug_print(f" 🗑️ 废液容器: '{waste_vessel}'")
debug_print(f" 📦 其他参数: {kwargs}")
debug_print("=" * 60)
action_sequence = []
# === 参数验证和标准化 ===
debug_print("步骤1: 参数验证和标准化...")
debug_print("🔍 步骤1: 参数验证和标准化...")
action_sequence.append(create_action_log(f"开始分离操作 - 容器: {vessel}", "🎬"))
action_sequence.append(create_action_log(f"分离目的: {purpose}", "🧪"))
action_sequence.append(create_action_log(f"产物相: {product_phase}", "📊"))
# 统一容器参数
final_vessel = vessel or separation_vessel
if not final_vessel:
debug_print("❌ 必须指定分离容器")
raise ValueError("必须指定分离容器 (vessel 或 separation_vessel)")
final_to_vessel = to_vessel or product_vessel
@@ -237,14 +347,18 @@ def generate_separate_protocol(
# 🔧 修复确保repeats至少为1
if repeats <= 0:
repeats = 1
debug_print(f"⚠️ repeats参数 <= 0自动设置为1")
debug_print(f"⚠️ 重复次数参数 <= 0自动设置为 1")
debug_print(f"标准化参数:")
debug_print(f" - 分离容器: '{final_vessel}'")
debug_print(f" - 产物容器: '{final_to_vessel}'")
debug_print(f" - 废液容器: '{final_waste_vessel}'")
debug_print(f" - 溶剂体积: {final_volume}mL")
debug_print(f" - 重复次数: {repeats}")
debug_print(f"🔧 标准化后的参数:")
debug_print(f" 🥼 分离容器: '{final_vessel}'")
debug_print(f" 🎯 产物容器: '{final_to_vessel}'")
debug_print(f" 🗑️ 废液容器: '{final_waste_vessel}'")
debug_print(f" 📏 溶剂体积: {final_volume}mL")
debug_print(f" 🔄 重复次数: {repeats}")
action_sequence.append(create_action_log(f"分离容器: {final_vessel}", "🧪"))
action_sequence.append(create_action_log(f"溶剂体积: {final_volume}mL", "📏"))
action_sequence.append(create_action_log(f"重复次数: {repeats}", "🔄"))
# 验证必需参数
if not purpose:
@@ -254,66 +368,147 @@ def generate_separate_protocol(
if purpose not in ["wash", "extract", "separate"]:
debug_print(f"⚠️ 未知的分离目的 '{purpose}',使用默认值 'separate'")
purpose = "separate"
action_sequence.append(create_action_log(f"未知目的,使用: {purpose}", "⚠️"))
if product_phase not in ["top", "bottom"]:
debug_print(f"⚠️ 未知的产物相 '{product_phase}',使用默认值 'top'")
product_phase = "top"
action_sequence.append(create_action_log(f"未知相别,使用: {product_phase}", "⚠️"))
debug_print("✅ 参数验证通过")
action_sequence.append(create_action_log("参数验证通过", ""))
# === 查找设备 ===
debug_print("步骤2: 查找设备...")
debug_print("🔍 步骤2: 查找设备...")
action_sequence.append(create_action_log("正在查找相关设备...", "🔍"))
# 查找分离器设备
separator_device = find_separator_device(G, final_vessel)
if not separator_device:
debug_print("⚠️ 未找到分离器设备,可能无法执行分离操作")
if separator_device:
action_sequence.append(create_action_log(f"找到分离器设备: {separator_device}", "🧪"))
else:
debug_print("⚠️ 未找到分离器设备,可能无法执行分离")
action_sequence.append(create_action_log("未找到分离器设备", "⚠️"))
# 查找搅拌器
stirrer_device = find_connected_stirrer(G, final_vessel)
if stirrer_device:
action_sequence.append(create_action_log(f"找到搅拌器: {stirrer_device}", "🌪️"))
else:
action_sequence.append(create_action_log("未找到搅拌器", "⚠️"))
# 查找溶剂容器(如果需要)
solvent_vessel = ""
if solvent and solvent.strip():
solvent_vessel = find_solvent_vessel(G, solvent)
if solvent_vessel:
action_sequence.append(create_action_log(f"找到溶剂容器: {solvent_vessel}", "💧"))
else:
action_sequence.append(create_action_log(f"未找到溶剂容器: {solvent}", "⚠️"))
debug_print(f"设备映射:")
debug_print(f" - 分离器设备: '{separator_device}'")
debug_print(f" - 溶剂容器: '{solvent_vessel}'")
debug_print(f"📊 设备配置:")
debug_print(f" 🧪 分离器设备: '{separator_device}'")
debug_print(f" 🌪️ 搅拌器设备: '{stirrer_device}'")
debug_print(f" 💧 溶剂容器: '{solvent_vessel}'")
# === 执行分离流程 ===
debug_print("步骤3: 执行分离流程...")
debug_print("🔍 步骤3: 执行分离流程...")
action_sequence.append(create_action_log("开始分离工作流程", "🎯"))
try:
for repeat_idx in range(repeats):
debug_print(f"3.{repeat_idx+1}: 第 {repeat_idx+1}/{repeats} 次分离")
cycle_num = repeat_idx + 1
debug_print(f"🔄 第{cycle_num}轮: 开始分离循环 {cycle_num}/{repeats}")
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 开始", "🔄"))
# 步骤3.1: 添加溶剂(如果需要)
if solvent_vessel and final_volume > 0:
debug_print(f"3.{repeat_idx+1}.1: 添加溶剂 {solvent} ({final_volume}mL)")
debug_print(f"🔄 第{cycle_num}轮 步骤1: 添加溶剂 {solvent} ({final_volume}mL)")
action_sequence.append(create_action_log(f"向分离容器添加 {final_volume}mL {solvent}", "💧"))
# 使用pump protocol添加溶剂
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent_vessel,
to_vessel=final_vessel,
volume=final_volume,
amount="",
time=0.0,
viscous=False,
rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=2.5,
transfer_flowrate=0.5,
rate_spec="",
event="",
through="",
**kwargs
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
try:
# 使用pump protocol添加溶剂
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=solvent_vessel,
to_vessel=final_vessel,
volume=final_volume,
amount="",
time=0.0,
viscous=False,
rinsing_solvent="",
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=2.5,
transfer_flowrate=0.5,
rate_spec="",
event="",
through="",
**kwargs
)
action_sequence.extend(pump_actions)
debug_print(f"✅ 溶剂添加完成,添加了 {len(pump_actions)} 个动作")
action_sequence.append(create_action_log(f"溶剂转移完成 ({len(pump_actions)} 个操作)", ""))
# 步骤3.2: 执行分离操作
except Exception as e:
debug_print(f"❌ 溶剂添加失败: {str(e)}")
action_sequence.append(create_action_log(f"溶剂添加失败: {str(e)}", ""))
else:
debug_print(f"🔄 第{cycle_num}轮 步骤1: 无需添加溶剂")
action_sequence.append(create_action_log("无需添加溶剂", "⏭️"))
# 步骤3.2: 启动搅拌(如果有搅拌器)
if stirrer_device and stir_time > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤2: 开始搅拌 ({stir_speed}rpm持续 {stir_time}s)")
action_sequence.append(create_action_log(f"开始搅拌: {stir_speed}rpm持续 {stir_time}s", "🌪️"))
action_sequence.append({
"device_id": stirrer_device,
"action_name": "start_stir",
"action_kwargs": {
"vessel": final_vessel,
"stir_speed": stir_speed,
"purpose": f"分离混合 - {purpose}"
}
})
# 搅拌等待
stir_minutes = stir_time / 60
action_sequence.append(create_action_log(f"搅拌中,持续 {stir_minutes:.1f} 分钟", "⏱️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": stir_time}
})
# 停止搅拌
action_sequence.append(create_action_log("停止搅拌器", "🛑"))
action_sequence.append({
"device_id": stirrer_device,
"action_name": "stop_stir",
"action_kwargs": {"vessel": final_vessel}
})
else:
debug_print(f"🔄 第{cycle_num}轮 步骤2: 无需搅拌")
action_sequence.append(create_action_log("无需搅拌", "⏭️"))
# 步骤3.3: 静置分层
if settling_time > 0:
debug_print(f"🔄 第{cycle_num}轮 步骤3: 静置分层 ({settling_time}s)")
settling_minutes = settling_time / 60
action_sequence.append(create_action_log(f"静置分层 ({settling_minutes:.1f} 分钟)", "⚖️"))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": settling_time}
})
else:
debug_print(f"🔄 第{cycle_num}轮 步骤3: 未指定静置时间")
action_sequence.append(create_action_log("未指定静置时间", "⏭️"))
# 步骤3.4: 执行分离操作
if separator_device:
debug_print(f"3.{repeat_idx+1}.2: 执行分离操作")
debug_print(f"🔄 第{cycle_num}轮 步骤4: 执行分离操作")
action_sequence.append(create_action_log(f"执行分离: 收集{product_phase}", "🧪"))
# 调用分离器设备的separate方法
separate_action = {
@@ -330,31 +525,44 @@ def generate_separate_protocol(
"solvent_volume": final_volume,
"through": through,
"repeats": 1, # 每次调用只做一次分离
"stir_time": stir_time,
"stir_time": 0, # 已经在上面完成
"stir_speed": stir_speed,
"settling_time": settling_time
"settling_time": 0 # 已经在上面完成
}
}
action_sequence.append(separate_action)
debug_print(f"✅ 分离操作添加完成")
debug_print(f"✅ 分离操作添加")
action_sequence.append(create_action_log("分离操作完成", ""))
# 收集结果
if final_to_vessel:
action_sequence.append(create_action_log(f"产物 ({product_phase}相) 收集到: {final_to_vessel}", "📦"))
if final_waste_vessel:
action_sequence.append(create_action_log(f"废相收集到: {final_waste_vessel}", "🗑️"))
else:
debug_print(f"3.{repeat_idx+1}.2: 无分离器设备,跳过分离操作")
debug_print(f"🔄 第{cycle_num}轮 步骤4: 无分离器设备,跳过分离")
action_sequence.append(create_action_log("无分离器设备可用", ""))
# 添加等待时间模拟分离
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": stir_time + settling_time}
"action_kwargs": {"time": 10.0}
})
# 等待间隔(除了最后一次)
# 循环间等待(除了最后一次)
if repeat_idx < repeats - 1:
debug_print(f"🔄 第{cycle_num}轮: 等待下一次循环...")
action_sequence.append(create_action_log("等待下一次循环...", ""))
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
else:
action_sequence.append(create_action_log(f"分离循环 {cycle_num}/{repeats} 完成", "🌟"))
except Exception as e:
debug_print(f"⚠️ 分离流程执行失败: {str(e)}")
debug_print(f" 分离工作流程执行失败: {str(e)}")
action_sequence.append(create_action_log(f"分离工作流程失败: {str(e)}", ""))
# 添加错误日志
action_sequence.append({
"device_id": "system",
@@ -365,22 +573,31 @@ def generate_separate_protocol(
})
# === 最终结果 ===
total_time = (stir_time + settling_time + 15) * repeats # 估算总时间
debug_print("=" * 60)
debug_print(f" 分离协议生成完成")
debug_print(f"📊 总动作数: {len(action_sequence)}")
debug_print(f"📋 处理总结:")
debug_print(f" - 分离容器: {final_vessel}")
debug_print(f" - 分离目的: {purpose}")
debug_print(f" - 产物相: {product_phase}")
debug_print(f" - 重复次数: {repeats}")
debug_print(f"🎉 分离协议生成完成")
debug_print(f"📊 协议统计:")
debug_print(f" 📋 总动作数: {len(action_sequence)}")
debug_print(f" ⏱️ 预计总时间: {total_time:.0f}s ({total_time/60:.1f} 分钟)")
debug_print(f" 🥼 分离容器: {final_vessel}")
debug_print(f" 🎯 分离目的: {purpose}")
debug_print(f" 📊 产物相: {product_phase}")
debug_print(f" 🔄 重复次数: {repeats}")
if solvent:
debug_print(f" - 溶剂: {solvent} ({final_volume}mL)")
debug_print(f" 💧 溶剂: {solvent} ({final_volume}mL)")
if final_to_vessel:
debug_print(f" - 产物容器: {final_to_vessel}")
debug_print(f" 🎯 产物容器: {final_to_vessel}")
if final_waste_vessel:
debug_print(f" - 废液容器: {final_waste_vessel}")
debug_print(f" 🗑️ 废液容器: {final_waste_vessel}")
debug_print("=" * 60)
# 添加完成日志
summary_msg = f"分离协议完成: {final_vessel} ({purpose}{repeats} 次循环)"
if solvent:
summary_msg += f",使用 {final_volume}mL {solvent}"
action_sequence.append(create_action_log(summary_msg, "🎉"))
return action_sequence
# === 便捷函数 ===
@@ -388,6 +605,7 @@ def generate_separate_protocol(
def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top",
product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
"""仅进行相分离(不添加溶剂)"""
debug_print(f"⚡ 快速相分离: {vessel} ({product_phase}相)")
return generate_separate_protocol(
G, vessel=vessel,
purpose="separate",
@@ -399,6 +617,7 @@ def separate_phases_only(G: nx.DiGraph, vessel: str, product_phase: str = "top",
def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
product_phase: str = "top", repeats: int = 1) -> List[Dict[str, Any]]:
"""用溶剂洗涤"""
debug_print(f"🧽 用{solvent}洗涤: {vessel} ({repeats} 次)")
return generate_separate_protocol(
G, vessel=vessel,
purpose="wash",
@@ -411,6 +630,7 @@ def wash_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[st
def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union[str, float],
product_phase: str = "bottom", repeats: int = 3) -> List[Dict[str, Any]]:
"""用溶剂萃取"""
debug_print(f"🧪 用{solvent}萃取: {vessel} ({repeats} 次)")
return generate_separate_protocol(
G, vessel=vessel,
purpose="extract",
@@ -423,6 +643,7 @@ def extract_with_solvent(G: nx.DiGraph, vessel: str, solvent: str, volume: Union
def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "top",
product_vessel: str = "", waste_vessel: str = "") -> List[Dict[str, Any]]:
"""水-有机相分离"""
debug_print(f"💧 水-有机相分离: {vessel} (有机相: {organic_phase})")
return generate_separate_protocol(
G, vessel=vessel,
purpose="separate",
@@ -434,15 +655,16 @@ def separate_aqueous_organic(G: nx.DiGraph, vessel: str, organic_phase: str = "t
# 测试函数
def test_separate_protocol():
"""测试分离协议的各种参数解析"""
print("=== SEPARATE PROTOCOL 增强版测试 ===")
debug_print("=== 分离协议增强中文版测试 ===")
# 测试体积解析
volumes = ["200 mL", "?", 100.0, "1 L", "500 μL"]
debug_print("🧪 测试体积解析...")
volumes = ["200 mL", "?", 100.0, "1 L", "500 μL", "未知", "50毫升"]
for vol in volumes:
result = parse_volume_input(vol)
print(f"体积解析: {vol} {result}mL")
debug_print(f"📊 体积解析结果: {vol} -> {result}mL")
print("✅ 测试完成")
debug_print("✅ 测试完成")
if __name__ == "__main__":
test_separate_protocol()

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import traceback
from typing import List, Sequence, Optional, Literal, Union, Iterator
import asyncio
@@ -117,7 +118,7 @@ class LiquidHandlerAbstract(LiquidHandler):
pass # This mode is not verified.
else:
if len(asp_vols) != len(targets):
raise ValueError("Length of `vols` must match `targets`.")
raise ValueError(f"Length of `asp_vols` {len(asp_vols)} must match `targets` {len(targets)}.")
tip = next(self.current_tip)
await self.pick_up_tips(tip)
@@ -160,6 +161,7 @@ class LiquidHandlerAbstract(LiquidHandler):
await self.discard_tips()
except Exception as e:
traceback.print_exc()
raise RuntimeError(f"Liquid addition failed: {e}") from e
# ---------------------------------------------------------------
@@ -183,7 +185,7 @@ class LiquidHandlerAbstract(LiquidHandler):
spread: Literal["wide", "tight", "custom"] = "wide",
is_96_well: bool = False,
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
mix_times: Optional[List(int)] = None,
mix_times: Optional[List[int]] = None,
mix_vol: Optional[int] = None,
mix_rate: Optional[int] = None,
mix_liquid_height: Optional[float] = None,

View File

@@ -1,23 +1,667 @@
import socket, json, contextlib
from typing import Any, List, Dict, Optional
import asyncio
import collections
import contextlib
import json
import socket
import time
from typing import Any, List, Dict, Optional, TypedDict, Union, Sequence, Iterator, Literal
from pylabrobot.liquid_handling import (
LiquidHandlerBackend,
Pickup,
SingleChannelAspiration,
Drop,
SingleChannelDispense,
PickupTipRack,
DropTipRack,
MultiHeadAspirationPlate,
)
from pylabrobot.liquid_handling.standard import (
MultiHeadAspirationContainer,
MultiHeadDispenseContainer,
MultiHeadDispensePlate,
ResourcePickup,
ResourceMove,
ResourceDrop,
)
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
class PRCXIError(RuntimeError):
"""Lilith 返回 Success=false 时抛出的业务异常"""
class PRCXI9300:
class Material(TypedDict): # 和Plate同关系
uuid: str
Code: Optional[str]
Name: Optional[str]
SummaryName: Optional[str]
PipetteHeight: Optional[int]
materialEnum: Optional[int]
def __init__(self, host: str = "127.0.0.1", port: int = 9999,
timeout: float = 10.0) -> None:
class WorkTablets(TypedDict):
Number: int
Code: str
Material: Dict[str, Any]
class MatrixInfo(TypedDict):
MatrixId: str
MatrixName: str
MatrixCount: int
WorkTablets: list[WorkTablets]
class PRCXI9300Deck(Deck):
"""PRCXI 9300 的专用 Deck 类,继承自 Deck。
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float):
super().__init__(name, size_x, size_y, size_z)
self.slots = [None] * 6 # PRCXI 9300 有 6 个槽位
class PRCXI9300Container(Plate):
"""PRCXI 9300 的专用 Deck 类,继承自 Deck。
该类定义了 PRCXI 9300 的工作台布局和槽位信息。
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float, category: str):
super().__init__(name, size_x, size_y, size_z, category=category, ordering=collections.OrderedDict())
self._unilabos_state = {}
def load_state(self, state: Dict[str, Any]) -> None:
"""从给定的状态加载工作台信息。"""
super().load_state(state)
self._unilabos_state = state
def serialize_state(self) -> Dict[str, Dict[str, Any]]:
data = super().serialize_state()
data.update(self._unilabos_state)
return data
class PRCXI9300Handler(LiquidHandlerAbstract):
@property
def reset_ok(self) -> bool:
"""检查设备是否已重置成功。"""
if self._unilabos_backend.debug:
return True
return self._unilabos_backend.is_reset_ok
def __init__(self, deck: Deck, host: str, port: int, timeout: float, setup=True, debug=False):
tablets_info = []
count = 0
for child in deck.children:
if "Material" in child._unilabos_state:
count += 1
tablets_info.append(
WorkTablets(Number=count, Code=f"T{count}", Material=child._unilabos_state["Material"])
)
self._unilabos_backend = PRCXI9300Backend(tablets_info, host, port, timeout, setup, debug)
super().__init__(backend=self._unilabos_backend, deck=deck)
async def create_protocol(
self,
protocol_name: str = "",
protocol_description: str = "",
protocol_version: str = "",
protocol_author: str = "",
protocol_date: str = "",
protocol_type: str = "",
none_keys: List[str] = [],
):
self._unilabos_backend.create_protocol(protocol_name)
async def run_protocol(self):
return self._unilabos_backend.run_protocol()
async def remove_liquid(
self,
vols: List[float],
sources: Sequence[Container],
waste_liquid: Optional[Container] = None,
*,
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
delays: Optional[List[int]] = None,
is_96_well: Optional[bool] = False,
top: Optional[List[float]] = None,
none_keys: List[str] = [],
):
return await super().remove_liquid(
vols,
sources,
waste_liquid,
use_channels=use_channels,
flow_rates=flow_rates,
offsets=offsets,
liquid_height=liquid_height,
blow_out_air_volume=blow_out_air_volume,
spread=spread,
delays=delays,
is_96_well=is_96_well,
top=top,
none_keys=none_keys,
)
async def add_liquid(
self,
asp_vols: Union[List[float], float],
dis_vols: Union[List[float], float],
reagent_sources: Sequence[Container],
targets: Sequence[Container],
*,
use_channels: Optional[List[int]] = None,
flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
is_96_well: bool = False,
delays: Optional[List[int]] = None,
mix_time: Optional[int] = None,
mix_vol: Optional[int] = None,
mix_rate: Optional[int] = None,
mix_liquid_height: Optional[float] = None,
none_keys: List[str] = [],
):
return await super().add_liquid(
asp_vols,
dis_vols,
reagent_sources,
targets,
use_channels=use_channels,
flow_rates=flow_rates,
offsets=offsets,
liquid_height=liquid_height,
blow_out_air_volume=blow_out_air_volume,
spread=spread,
is_96_well=is_96_well,
delays=delays,
mix_time=mix_time,
mix_vol=mix_vol,
mix_rate=mix_rate,
mix_liquid_height=mix_liquid_height,
none_keys=none_keys,
)
async def transfer_liquid(
self,
sources: Sequence[Container],
targets: Sequence[Container],
tip_racks: Sequence[TipRack],
*,
use_channels: Optional[List[int]] = None,
asp_vols: Union[List[float], float],
dis_vols: Union[List[float], float],
asp_flow_rates: Optional[List[Optional[float]]] = None,
dis_flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
touch_tip: bool = False,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Literal["wide", "tight", "custom"] = "wide",
is_96_well: bool = False,
mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
mix_times: Optional[List[int]] = None,
mix_vol: Optional[int] = None,
mix_rate: Optional[int] = None,
mix_liquid_height: Optional[float] = None,
delays: Optional[List[int]] = None,
none_keys: List[str] = [],
):
return await super().transfer_liquid(
sources,
targets,
tip_racks,
use_channels=use_channels,
asp_vols=asp_vols,
dis_vols=dis_vols,
asp_flow_rates=asp_flow_rates,
dis_flow_rates=dis_flow_rates,
offsets=offsets,
touch_tip=touch_tip,
liquid_height=liquid_height,
blow_out_air_volume=blow_out_air_volume,
spread=spread,
is_96_well=is_96_well,
mix_stage=mix_stage,
mix_times=mix_times,
mix_vol=mix_vol,
mix_rate=mix_rate,
mix_liquid_height=mix_liquid_height,
delays=delays,
none_keys=none_keys,
)
async def custom_delay(self, seconds=0, msg=None):
return await super().custom_delay(seconds, msg)
async def touch_tip(self, targets: Sequence[Container]):
return await super().touch_tip(targets)
async def mix(
self,
targets: Sequence[Container],
mix_time: int = None,
mix_vol: Optional[int] = None,
height_to_bottom: Optional[float] = None,
offsets: Optional[Coordinate] = None,
mix_rate: Optional[float] = None,
none_keys: List[str] = [],
):
return await self._unilabos_backend.mix(targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys)
def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]:
return super().iter_tips(tip_racks)
async def pick_up_tips(self, tip_spots: List[TipSpot], use_channels: Optional[List[int]] = None,
offsets: Optional[List[Coordinate]] = None, **backend_kwargs):
return await super().pick_up_tips(tip_spots, use_channels, offsets, **backend_kwargs)
async def aspirate(self, resources: Sequence[Container], vols: List[float],
use_channels: Optional[List[int]] = None, flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Literal["wide", "tight", "custom"] = "wide", **backend_kwargs):
return await super().aspirate(resources, vols, use_channels, flow_rates, offsets, liquid_height,
blow_out_air_volume, spread, **backend_kwargs)
async def drop_tips(self, tip_spots: Sequence[Union[TipSpot, Trash]], use_channels: Optional[List[int]] = None,
offsets: Optional[List[Coordinate]] = None, allow_nonzero_volume: bool = False,
**backend_kwargs):
return await super().drop_tips(tip_spots, use_channels, offsets, allow_nonzero_volume, **backend_kwargs)
async def dispense(self, resources: Sequence[Container], vols: List[float],
use_channels: Optional[List[int]] = None, flow_rates: Optional[List[Optional[float]]] = None,
offsets: Optional[List[Coordinate]] = None,
liquid_height: Optional[List[Optional[float]]] = None,
blow_out_air_volume: Optional[List[Optional[float]]] = None,
spread: Literal["wide", "tight", "custom"] = "wide", **backend_kwargs):
return await super().dispense(resources, vols, use_channels, flow_rates, offsets, liquid_height,
blow_out_air_volume, spread, **backend_kwargs)
async def discard_tips(self, use_channels: Optional[List[int]] = None, allow_nonzero_volume: bool = True,
offsets: Optional[List[Coordinate]] = None, **backend_kwargs):
return await super().discard_tips(use_channels, allow_nonzero_volume, offsets, **backend_kwargs)
def set_tiprack(self, tip_racks: Sequence[TipRack]):
super().set_tiprack(tip_racks)
async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0):
return await super().move_to(well, dis_to_top, channel)
class PRCXI9300Backend(LiquidHandlerBackend):
"""PRCXI 9300 的后端实现,继承自 LiquidHandlerBackend。
该类提供了与 PRCXI 9300 设备进行通信的基本方法,包括方案管理、自动化控制、运行状态查询等。
"""
_num_channels = 8 # 默认通道数为 8
_is_reset_ok = False
@property
def is_reset_ok(self) -> bool:
self._is_reset_ok = self.api_client.get_reset_status()
return self._is_reset_ok
matrix_info: MatrixInfo
protocol_name: str
steps_todo_list = []
def __init__(
self,
tablets_info: list[WorkTablets],
host: str = "127.0.0.1",
port: int = 9999,
timeout: float = 10.0,
setup=True,
debug=False,
) -> None:
super().__init__()
self.tablets_info = tablets_info
self.api_client = PRCXI9300Api(host, port, timeout, debug)
self.host, self.port, self.timeout = host, port, timeout
self._num_channels = 8
self._execute_setup = setup
self.debug = debug
def create_protocol(self, protocol_name):
self.protocol_name = protocol_name
self.steps_todo_list = []
def run_protocol(self):
assert self.is_reset_ok, "PRCXI9300Backend is not reset successfully. Please call setup() first."
run_time = time.time()
self.matrix_info = MatrixInfo(
MatrixId=f"{int(run_time)}",
MatrixName=f"protocol_{run_time}",
MatrixCount=len(self.tablets_info),
WorkTablets=self.tablets_info,
)
#print(json.dumps(self.matrix_info, indent=2))
res = self.api_client.add_WorkTablet_Matrix(self.matrix_info)
assert res["Success"], f"Failed to create matrix: {res.get('Message', 'Unknown error')}"
print(f"PRCXI9300Backend created matrix with ID: {self.matrix_info['MatrixId']}, result: {res}")
solution_id = self.api_client.add_solution(
f"protocol_{run_time}", self.matrix_info["MatrixId"], self.steps_todo_list
)
print(f"PRCXI9300Backend created solution with ID: {solution_id}")
self.api_client.load_solution(solution_id)
return self.api_client.start()
@classmethod
def check_channels(cls, use_channels: List[int]) -> List[int]:
"""检查通道是否符合要求PRCXI9300Backend 只支持所有 8 个通道。"""
if use_channels != [0, 1, 2, 3, 4, 5, 6, 7]:
print("PRCXI9300Backend only supports all 8 channels, using default [0, 1, 2, 3, 4, 5, 6, 7].")
return [0, 1, 2, 3, 4, 5, 6, 7]
return use_channels
async def setup(self):
await super().setup()
try:
if self._execute_setup:
self.api_client.call("IAutomation", "Reset")
while not self.is_reset_ok:
print("Waiting for PRCXI9300 to reset...")
await asyncio.sleep(1)
print("PRCXI9300 reset successfully.")
except ConnectionRefusedError as e:
raise RuntimeError(
f"Failed to connect to PRCXI9300 API at {self.host}:{self.port}. "
"Please ensure the PRCXI9300 service is running."
) from e
async def stop(self):
self.api_client.call("IAutomation", "Stop")
async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int] = None):
"""Pick up tips from the specified resource."""
if len(ops) != 8:
raise ValueError(f"PRCXI9300Backend pick_up_tips: Expected 8 pickups, got {len(ops)}")
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
step = self.api_client.Load(
"Left",
dosage=0,
plate_no=PlateNo,
is_whole_plate=False,
hole_row=1,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def drop_tips(self, ops: List[Drop], use_channels: List[int] = None):
"""Pick up tips from the specified resource."""
if len(ops) != 8:
raise ValueError(f"PRCXI9300Backend drop_tips: Expected 8 pickups, got {len(ops)}")
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All drop_tips must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError("All drop_tips must be from the same tip column. Found different columns: " + str(tip_columns))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
step = self.api_client.UnLoad(
"Left",
dosage=0,
plate_no=PlateNo,
is_whole_plate=False,
hole_row=1,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def mix(
self,
targets: Sequence[Container],
mix_time: int = None,
mix_vol: Optional[int] = None,
height_to_bottom: Optional[float] = None,
offsets: Optional[Coordinate] = None,
mix_rate: Optional[float] = None,
none_keys: List[str] = [],
):
"""Mix liquid in the specified resources."""
if len(targets) != 8:
raise ValueError(f"PRCXI9300Backend aspirate: Expected 8 aspirate, got {len(targets)}")
plate_indexes = []
for op in targets:
deck = op.parent.parent.parent
plate = op.parent.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in targets:
tipspot_index = op.parent.children.index(op)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
assert mix_time > 0
step = self.api_client.Blending(
"Left",
dosage=mix_vol,
plate_no=PlateNo,
is_whole_plate=False,
hole_row=1,
hole_col=hole_col,
blending_times=mix_time,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def aspirate(self, ops: List[SingleChannelAspiration], use_channels: List[int] = None):
"""Aspirate liquid from the specified resources."""
if len(ops) != 8:
raise ValueError(f"PRCXI9300Backend aspirate: Expected 8 aspirate, got {len(ops)}")
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All pickups must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
volumes = [op.volume for op in ops]
if len(set(volumes)) != 1:
raise ValueError("All aspirate volumes must be the same. Found different volumes: " + str(volumes))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
step = self.api_client.Imbibing(
"Left",
dosage=int(volumes[0]),
plate_no=PlateNo,
is_whole_plate=False,
hole_row=1,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[int] = None):
"""Dispense liquid into the specified resources."""
if len(ops) != 8:
raise ValueError(f"PRCXI9300Backend dispense: Expected 8 dispense, got {len(ops)}")
plate_indexes = []
for op in ops:
plate = op.resource.parent.parent
deck = plate.parent
plate_index = deck.children.index(plate)
plate_indexes.append(plate_index)
if len(set(plate_indexes)) != 1:
raise ValueError("All dispense must be from the same plate. Found different plates: " + str(plate_indexes))
tip_columns = []
for op in ops:
tipspot = op.resource
tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1:
raise ValueError("All dispense must be from the same tip column. Found different columns: " + str(tip_columns))
volumes = [op.volume for op in ops]
if len(set(volumes)) != 1:
raise ValueError("All dispense volumes must be the same. Found different volumes: " + str(volumes))
PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1
step = self.api_client.Tapping(
"Left",
dosage=int(volumes[0]),
plate_no=PlateNo,
is_whole_plate=False,
hole_row=1,
hole_col=hole_col,
blending_times=0,
balance_height=0,
plate_or_hole=f"H{hole_col}-8,T{PlateNo}",
hole_numbers="1,2,3,4,5,6,7,8",
)
self.steps_todo_list.append(step)
async def pick_up_tips96(self, pickup: PickupTipRack):
raise NotImplementedError("The PRCXI backend does not support the 96 head.")
async def drop_tips96(self, drop: DropTipRack):
raise NotImplementedError("The PRCXI backend does not support the 96 head.")
async def aspirate96(self, aspiration: Union[MultiHeadAspirationPlate, MultiHeadAspirationContainer]):
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]):
raise NotImplementedError("The Opentrons backend does not support the 96 head.")
async def pick_up_resource(self, pickup: ResourcePickup):
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
async def move_picked_up_resource(self, move: ResourceMove):
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
async def drop_resource(self, drop: ResourceDrop):
raise NotImplementedError("The Opentrons backend does not support the robotic arm.")
def can_pick_up_tip(self, channel_idx: int, tip: Tip) -> bool:
return True # PRCXI9300Backend does not have tip compatibility issues
def serialize(self) -> dict:
raise NotImplementedError()
@property
def num_channels(self) -> int:
return self._num_channels
class PRCXI9300Api:
def __init__(self, host: str = "127.0.0.1", port: int = 9999, timeout: float = 10.0, debug: bool = False) -> None:
self.host, self.port, self.timeout = host, port, timeout
self.debug = debug
@staticmethod
def _len_prefix(n: int) -> bytes:
return bytes.fromhex(format(n, "016x"))
def _raw_request(self, payload: str) -> str:
if self.debug:
return " "
with contextlib.closing(socket.socket()) as sock:
sock.settimeout(self.timeout)
sock.connect((self.host, self.port))
@@ -34,13 +678,26 @@ class PRCXI9300:
chunks.append(chunk)
return b"".join(chunks).decode()
def _call(self, service: str, method: str,
params: Optional[list] = None) -> Any:
# ---------------------------------------------------- 方案相关ISolution
def list_solutions(self) -> List[Dict[str, Any]]:
"""GetSolutionList"""
return self.call("ISolution", "GetSolutionList")
def load_solution(self, solution_id: str) -> bool:
"""LoadSolution"""
return self.call("ISolution", "LoadSolution", [solution_id])
def add_solution(self, name: str, matrix_id: str, steps: List[Dict[str, Any]]) -> str:
"""AddSolution → 返回新方案 GUID"""
return self.call("ISolution", "AddSolution", [name, matrix_id, steps])
# ---------------------------------------------------- 自动化控制IAutomation
def start(self) -> bool:
return self.call("IAutomation", "Start")
def call(self, service: str, method: str, params: Optional[list] = None) -> Any:
payload = json.dumps(
{"ServiceName": service,
"MethodName": method,
"Paramters": params or []},
separators=(",", ":")
{"ServiceName": service, "MethodName": method, "Paramters": params or []}, separators=(",", ":")
)
resp = json.loads(self._raw_request(payload))
if not resp.get("Success", False):
@@ -51,84 +708,55 @@ class PRCXI9300:
except (TypeError, json.JSONDecodeError):
return data
# ---------------------------------------------------- 方案相关ISolution
def list_solutions(self) -> List[Dict[str, Any]]:
"""GetSolutionList"""
return self._call("ISolution", "GetSolutionList")
def load_solution(self, solution_id: str) -> bool:
"""LoadSolution"""
return self._call("ISolution", "LoadSolution", [solution_id])
def add_solution(self, name: str, matrix_id: str,
steps: List[Dict[str, Any]]) -> str:
"""AddSolution → 返回新方案 GUID"""
return self._call("ISolution", "AddSolution",
[name, matrix_id, steps])
# ---------------------------------------------------- 自动化控制IAutomation
def start(self) -> bool:
return self._call("IAutomation", "Start")
def stop(self) -> bool:
"""Stop"""
return self._call("IAutomation", "Stop")
def reset(self) -> bool:
"""Reset"""
return self._call("IAutomation", "Reset")
def pause(self) -> bool:
"""Pause"""
return self._call("IAutomation", "Pause")
return self.call("IAutomation", "Pause")
def resume(self) -> bool:
"""Resume"""
return self._call("IAutomation", "Resume")
return self.call("IAutomation", "Resume")
def get_error_code(self) -> Optional[str]:
"""GetErrorCode"""
return self._call("IAutomation", "GetErrorCode")
return self.call("IAutomation", "GetErrorCode")
def get_reset_status(self) -> Optional[str]:
"""GetErrorCode"""
res = self.call("IAutomation", "GetResetStatus")
return not res
def clear_error_code(self) -> bool:
"""RemoveErrorCodet"""
return self._call("IAutomation", "RemoveErrorCodet")
return self.call("IAutomation", "RemoveErrorCodet")
# ---------------------------------------------------- 运行状态IMachineState
def step_state_list(self) -> List[Dict[str, Any]]:
"""GetStepStateList"""
return self._call("IMachineState", "GetStepStateList")
return self.call("IMachineState", "GetStepStateList")
def step_status(self, seq_num: int) -> Dict[str, Any]:
"""GetStepStatus"""
return self._call("IMachineState", "GetStepStatus", [seq_num])
return self.call("IMachineState", "GetStepStatus", [seq_num])
def step_state(self, seq_num: int) -> Dict[str, Any]:
"""GetStepState"""
return self._call("IMachineState", "GetStepState", [seq_num])
return self.call("IMachineState", "GetStepState", [seq_num])
def axis_location(self, axis_num: int = 1) -> Dict[str, Any]:
"""GetLocation"""
return self._call("IMachineState", "GetLocation", [axis_num])
return self.call("IMachineState", "GetLocation", [axis_num])
# ---------------------------------------------------- 版位矩阵IMatrix
def list_matrices(self) -> List[Dict[str, Any]]:
"""GetWorkTabletMatrices"""
return self._call("IMatrix", "GetWorkTabletMatrices")
return self.call("IMatrix", "GetWorkTabletMatrices")
def matrix_by_id(self, matrix_id: str) -> Dict[str, Any]:
"""GetWorkTabletMatrixById"""
return self._call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id])
def add_WorkTablet_Matrix(self,matrix):
return self._call("IMatrix", "AddWorkTabletMatrix", [matrix])
# ---------------------------------------------------- 一键运行
def run_solution(self, solution_id: str, channel_idx: int = 1) -> None:
self.load_solution(solution_id)
self.start(channel_idx)
# ---------------------------------------------------- 单点动作
def add_WorkTablet_Matrix(self, matrix: MatrixInfo):
return self.call("IMatrix", "AddWorkTabletMatrix", [matrix])
def Load(
self,
@@ -147,7 +775,7 @@ class PRCXI9300:
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense"
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": axis,
@@ -166,7 +794,7 @@ class PRCXI9300:
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method
"LiquidDispensingMethod": liquid_method,
}
def Imbibing(
@@ -186,7 +814,7 @@ class PRCXI9300:
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense"
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": axis,
@@ -205,10 +833,9 @@ class PRCXI9300:
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method
"LiquidDispensingMethod": liquid_method,
}
def Tapping(
self,
axis: str,
@@ -226,7 +853,7 @@ class PRCXI9300:
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense"
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": axis,
@@ -245,10 +872,9 @@ class PRCXI9300:
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method
"LiquidDispensingMethod": liquid_method,
}
def Blending(
self,
axis: str,
@@ -266,7 +892,7 @@ class PRCXI9300:
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense"
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": axis,
@@ -285,7 +911,7 @@ class PRCXI9300:
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method
"LiquidDispensingMethod": liquid_method,
}
def UnLoad(
@@ -305,7 +931,7 @@ class PRCXI9300:
assist_fun3: str = "",
assist_fun4: str = "",
assist_fun5: str = "",
liquid_method: str = "NormalDispense"
liquid_method: str = "NormalDispense",
) -> Dict[str, Any]:
return {
"StepAxis": axis,
@@ -324,5 +950,103 @@ class PRCXI9300:
"AssistFun4": assist_fun4,
"AssistFun5": assist_fun5,
"HoleNumbers": hole_numbers,
"LiquidDispensingMethod": liquid_method
"LiquidDispensingMethod": liquid_method,
}
if __name__ == "__main__":
# Example usage
deck = PRCXI9300Deck(name="PRCXI Deck", size_x=100, size_y=100, size_z=100)
plate1 = PRCXI9300Container(name="rackT1", size_x=50, size_y=50, size_z=10, category="plate")
plate1.load_state({
"Material": {
"uuid": "80652665f6a54402b2408d50b40398df",
"Code": "ZX-001-1000",
"Name": "1000μL Tip头",
"SummaryName": "1000μL Tip头",
"PipetteHeight": 100,
"materialEnum": 1
}
})
plate2 = PRCXI9300Container(name="plateT2", size_x=50, size_y=50, size_z=10, category="plate")
plate2.load_state({
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
}
})
plate3 = PRCXI9300Container(name="plateT3", size_x=50, size_y=50, size_z=10, category="plate")
plate3.load_state({
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
}
})
plate4 = PRCXI9300Container(name="rackT4", size_x=50, size_y=50, size_z=10, category="plate")
plate4.load_state({
"Material": {
"uuid": "80652665f6a54402b2408d50b40398df",
"Code": "ZX-001-1000",
"Name": "1000μL Tip头",
"SummaryName": "1000μL Tip头",
"PipetteHeight": 100,
"materialEnum": 1
}
})
plate5 = PRCXI9300Container(name="plateT5", size_x=50, size_y=50, size_z=10, category="plate")
plate5.load_state({
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
}
})
plate6 = PRCXI9300Container(name="plateT6", size_x=50, size_y=50, size_z=10, category="plate")
plate6.load_state({
"Material": {
"uuid": "57b1e4711e9e4a32b529f3132fc5931f",
}
})
from pylabrobot.resources.opentrons.tip_racks import tipone_96_tiprack_200ul
from pylabrobot.resources.opentrons.plates import corning_96_wellplate_360ul_flat
tip_rack = tipone_96_tiprack_200ul("TipRack")
well_containers = corning_96_wellplate_360ul_flat("Plate")
# from pprint import pprint
# pprint(well_containers.children)
plate1.assign_child_resource(tip_rack, location=Coordinate(0, 0, 0))
plate2.assign_child_resource(well_containers, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate1, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate2, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate3, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate4, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate5, location=Coordinate(0, 0, 0))
deck.assign_child_resource(plate6, location=Coordinate(0, 0, 0))
handler = PRCXI9300Handler(deck=deck, host="192.168.3.9", port=9999, timeout=10.0, setup=False, debug=True)
handler.set_tiprack([tip_rack]) # Set the tip rack for the handler
asyncio.run(handler.setup()) # Initialize the handler and setup the connection
asyncio.run(handler.create_protocol(protocol_name="Test Protocol")) # Initialize the backend and setup the connection
asyncio.run(handler.pick_up_tips(tip_rack.children[:8],[0,1,2,3,4,5,6,7]))
asyncio.run(handler.aspirate(well_containers.children[:8],[50]*8, [0,1,2,3,4,5,6,7]))
asyncio.run(handler.dispense(well_containers.children[:8],[50]*8,[0,1,2,3,4,5,6,7]))
asyncio.run(handler.drop_tips(tip_rack.children[8:16],[0,1,2,3,4,5,6,7]))
asyncio.run(handler.mix(well_containers.children[:8], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# asyncio.run(handler.add_liquid(
# asp_vols=[100]*8,
# dis_vols=[100]*8,
# reagent_sources=well_containers.children[-8:],
# targets=well_containers.children[:8],
# use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# flow_rates=[None] * 8,
# offsets=[Coordinate(0, 0, 0)] * 8,
# liquid_height=[None] * 8,
# blow_out_air_volume=[None] * 8,
# spread="wide",
# ))
# input("pick_up_tips add step")
asyncio.run(handler.run_protocol()) # Run the protocol
# input("Running protocol...")
# input("Press Enter to continue...") # Wait for user input before proceeding
# print("PRCXI9300Handler initialized with deck and host settings.")

View File

@@ -17,7 +17,7 @@ class MoveitInterface:
tf_buffer: Buffer
tf_listener: TransformListener
def __init__(self, moveit_type, joint_poses, rotation=None, device_config=None):
def __init__(self, moveit_type, joint_poses, rotation=None, device_config=None, **kwargs):
self.device_config = device_config
self.rotation = rotation
self.data_config = json.load(

View File

@@ -101,7 +101,7 @@ class VirtualMultiwayValve:
self._target_position = pos
# 模拟阀门切换时间
switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.1
switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5
if switch_time > 0:
self.logger.info(f"⏱️ 阀门移动中... 预计用时: {switch_time:.1f}秒 🔄")
@@ -172,32 +172,32 @@ class VirtualMultiwayValve:
def is_at_position(self, position: int) -> bool:
"""检查是否在指定位置 🎯"""
result = self._current_position == position
self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}")
# 删除debug日志self.logger.debug(f"🎯 位置检查: 当前={self._current_position}, 目标={position}, 匹配={result}")
return result
def is_at_pump_position(self) -> bool:
"""检查是否在transfer pump位置 🚰"""
result = self._current_position == 0
pump_status = "" if result else ""
self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})")
# 删除debug日志pump_status = "是" if result else "否"
# 删除debug日志self.logger.debug(f"🚰 泵位置检查: {pump_status} (当前位置: {self._current_position})")
return result
def is_at_port(self, port_number: int) -> bool:
"""检查是否在指定端口位置 🔌"""
result = self._current_position == port_number
port_status = "" if result else ""
self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
# 删除debug日志port_status = "是" if result else "否"
# 删除debug日志self.logger.debug(f"🔌 端口{port_number}检查: {port_status} (当前位置: {self._current_position})")
return result
def get_available_positions(self) -> list:
"""获取可用位置列表 📋"""
positions = list(range(0, self.max_positions + 1))
self.logger.debug(f"📋 可用位置: {positions}")
# 删除debug日志self.logger.debug(f"📋 可用位置: {positions}")
return positions
def get_available_ports(self) -> Dict[int, str]:
"""获取可用端口映射 🗺️"""
self.logger.debug(f"🗺️ 端口映射: {self.position_map}")
# 删除debug日志self.logger.debug(f"🗺️ 端口映射: {self.position_map}")
return self.position_map.copy()
def reset(self):
@@ -229,7 +229,7 @@ class VirtualMultiwayValve:
else:
flow_path = f"🔌 端口 {self._current_position} 已连接 ({current_port})"
self.logger.debug(f"🌊 当前流路: {flow_path}")
# 删除debug日志self.logger.debug(f"🌊 当前流路: {flow_path}")
return flow_path
def get_info(self) -> dict:
@@ -247,7 +247,7 @@ class VirtualMultiwayValve:
"position_map": self.position_map
}
self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}")
# 删除debug日志self.logger.debug(f"📊 阀门信息: 位置={self._current_position}, 状态={self._status}, 端口={self.get_current_port()}")
return info
def __str__(self):
@@ -264,7 +264,7 @@ class VirtualMultiwayValve:
Args:
command: 目标位置 (0-8) 或位置字符串
"""
self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
# 删除debug日志self.logger.debug(f"🎯 兼容性调用: set_valve_position({command})")
return self.set_position(command)

View File

@@ -12,7 +12,7 @@ class VirtualPumpMode(Enum):
class VirtualTransferPump:
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件"""
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
"""
@@ -42,20 +42,31 @@ class VirtualTransferPump:
self._max_velocity = 5.0 # float
self._current_volume = 0.0 # float
# 🚀 新增:快速模式设置 - 大幅缩短执行时间
self._fast_mode = True # 是否启用快速模式
self._fast_move_time = 1.0 # 快速移动时间(秒)
self._fast_dispense_time = 1.0 # 快速喷射时间(秒)
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
print(f"🚰 === 虚拟转移泵 {self.device_id} 已创建 === ✨")
print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s")
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
async def initialize(self) -> bool:
"""初始化虚拟泵"""
self.logger.info(f"Initializing virtual pump {self.device_id}")
"""初始化虚拟泵 🚀"""
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id}")
self._status = "Idle"
self._position = 0.0
self._current_volume = 0.0
self.logger.info(f"✅ 转移泵 {self.device_id} 初始化完成 🚰")
return True
async def cleanup(self) -> bool:
"""清理虚拟泵"""
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
"""清理虚拟泵 🧹"""
self.logger.info(f"🧹 清理虚拟转移泵 {self.device_id} 🔚")
self._status = "Idle"
self.logger.info(f"✅ 转移泵 {self.device_id} 清理完成 💤")
return True
# 基本属性
@@ -65,12 +76,12 @@ class VirtualTransferPump:
@property
def position(self) -> float:
"""当前柱塞位置 (ml)"""
"""当前柱塞位置 (ml) 📍"""
return self._position
@property
def current_volume(self) -> float:
"""当前注射器中的体积 (ml)"""
"""当前注射器中的体积 (ml) 💧"""
return self._current_volume
@property
@@ -82,22 +93,50 @@ class VirtualTransferPump:
return self._transfer_rate
def set_max_velocity(self, velocity: float):
"""设置最大速度 (ml/s)"""
"""设置最大速度 (ml/s) 🌊"""
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
self.logger.info(f"Set max velocity to {self._max_velocity} ml/s")
self.logger.info(f"🌊 设置最大速度为 {self._max_velocity} mL/s")
def get_status(self) -> str:
"""获取泵状态"""
"""获取泵状态 📋"""
return self._status
async def _simulate_operation(self, duration: float):
"""模拟操作延时"""
"""模拟操作延时 ⏱️"""
self._status = "Busy"
await asyncio.sleep(duration)
self._status = "Idle"
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
"""计算操作持续时间"""
"""
计算操作持续时间 ⏰
🚀 快速模式:保留计算逻辑用于日志显示,但实际使用固定的快速时间
"""
if velocity is None:
velocity = self._max_velocity
# 📊 计算理论时间(用于日志显示)
theoretical_duration = abs(volume) / velocity
# 🚀 如果启用快速模式,使用固定的快速时间
if self._fast_mode:
# 根据操作类型选择快速时间
if abs(volume) > 0.1: # 大于0.1mL的操作
actual_duration = self._fast_move_time
else: # 很小的操作
actual_duration = 0.5
self.logger.debug(f"⚡ 快速模式: 理论时间 {theoretical_duration:.2f}s → 实际时间 {actual_duration:.2f}s")
return actual_duration
else:
# 正常模式使用理论时间
return theoretical_duration
def _calculate_display_duration(self, volume: float, velocity: float = None) -> float:
"""
计算显示用的持续时间(用于日志) 📊
这个函数返回理论计算时间,用于日志显示
"""
if velocity is None:
velocity = self._max_velocity
return abs(volume) / velocity
@@ -105,7 +144,7 @@ class VirtualTransferPump:
# 新的set_position方法 - 专门用于SetPumpPosition动作
async def set_position(self, position: float, max_velocity: float = None):
"""
移动到绝对位置 - 专门用于SetPumpPosition动作
移动到绝对位置 - 专门用于SetPumpPosition动作 🎯
Args:
position (float): 目标位置 (ml)
@@ -122,56 +161,107 @@ class VirtualTransferPump:
# 限制位置在有效范围内
target_position = max(0.0, min(float(self.max_volume), target_position))
# 计算移动距离和时间
# 计算移动距离
volume_to_move = abs(target_position - self._position)
duration = self._calculate_duration(volume_to_move, velocity)
self.logger.info(f"SET_POSITION: Moving to {target_position} ml (current: {self._position} ml), velocity: {velocity} ml/s")
# 📊 计算显示用的时间(用于日志)
display_duration = self._calculate_display_duration(volume_to_move, velocity)
# 模拟移动过程
start_position = self._position
steps = 10 if duration > 0.1 else 1 # 如果移动距离很小只用1步
step_duration = duration / steps if steps > 1 else duration
# ⚡ 计算实际执行时间(快速模式)
actual_duration = self._calculate_duration(volume_to_move, velocity)
for i in range(steps + 1):
# 计算当前位置和进度
progress = (i / steps) * 100 if steps > 0 else 100
current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position
# 🎯 确定操作类型和emoji
if target_position > self._position:
operation_type = "吸液"
operation_emoji = "📥"
elif target_position < self._position:
operation_type = "排液"
operation_emoji = "📤"
else:
operation_type = "保持"
operation_emoji = "📍"
# 更新状态
self._status = "Moving" if i < steps else "Idle"
self._position = current_pos
self._current_volume = current_pos
self.logger.info(f"🎯 SET_POSITION: {operation_type} {operation_emoji}")
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {target_position:.2f}mL (移动 {volume_to_move:.2f}mL)")
self.logger.info(f" 🌊 速度: {velocity:.2f} mL/s")
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
# 等待一小步时间
if i < steps and step_duration > 0:
await asyncio.sleep(step_duration)
if self._fast_mode:
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
# 🚀 模拟移动过程
if volume_to_move > 0.01: # 只有当移动距离足够大时才显示进度
start_position = self._position
steps = 5 if actual_duration > 0.5 else 2 # 根据实际时间调整步数
step_duration = actual_duration / steps
self.logger.info(f"🚀 开始{operation_type}... {operation_emoji}")
for i in range(steps + 1):
# 计算当前位置和进度
progress = (i / steps) * 100 if steps > 0 else 100
current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position
# 更新状态
if i < steps:
self._status = f"{operation_type}"
status_emoji = "🔄"
else:
self._status = "Idle"
status_emoji = ""
self._position = current_pos
self._current_volume = current_pos
# 显示进度每25%或最后一步)
if i == 0:
self.logger.debug(f" 🔄 {operation_type}开始: {progress:.0f}%")
elif progress >= 50 and i == steps // 2:
self.logger.debug(f" 🔄 {operation_type}进度: {progress:.0f}%")
elif i == steps:
self.logger.info(f"{operation_type}完成: {progress:.0f}% | 当前位置: {current_pos:.2f}mL")
# 等待一小步时间
if i < steps and step_duration > 0:
await asyncio.sleep(step_duration)
else:
# 移动距离很小,直接完成
self._position = target_position
self._current_volume = target_position
self.logger.info(f" 📍 微调完成: {target_position:.2f}mL")
# 确保最终位置准确
self._position = target_position
self._current_volume = target_position
self._status = "Idle"
self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml")
# 📊 最终状态日志
if volume_to_move > 0.01:
self.logger.info(f"🎉 SET_POSITION 完成! 📍 最终位置: {self._position:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
# 返回符合action定义的结果
return {
"success": True,
"message": f"Successfully moved to position {self._position} ml"
"message": f"✅ 成功移动到位置 {self._position:.2f}mL ({operation_type})",
"final_position": self._position,
"final_volume": self._current_volume,
"operation_type": operation_type
}
except Exception as e:
error_msg = f"Failed to set position: {str(e)}"
error_msg = f"❌ 设置位置失败: {str(e)}"
self.logger.error(error_msg)
return {
"success": False,
"message": error_msg
"message": error_msg,
"final_position": self._position,
"final_volume": self._current_volume
}
# 其他泵操作方法
async def pull_plunger(self, volume: float, velocity: float = None):
"""
拉取柱塞(吸液)
拉取柱塞(吸液) 📥
Args:
volume (float): 要拉取的体积 (ml)
@@ -181,23 +271,29 @@ class VirtualTransferPump:
actual_volume = new_position - self._position
if actual_volume <= 0:
self.logger.warning("Cannot pull - already at maximum volume")
self.logger.warning("⚠️ 无法吸液 - 已达到最大容量")
return
duration = self._calculate_duration(actual_volume, velocity)
display_duration = self._calculate_display_duration(actual_volume, velocity)
actual_duration = self._calculate_duration(actual_volume, velocity)
self.logger.info(f"Pulling {actual_volume} ml (from {self._position} to {new_position})")
self.logger.info(f"📥 开始吸液: {actual_volume:.2f}mL")
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
await self._simulate_operation(duration)
if self._fast_mode:
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
await self._simulate_operation(actual_duration)
self._position = new_position
self._current_volume = new_position
self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml")
self.logger.info(f"✅ 吸液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
async def push_plunger(self, volume: float, velocity: float = None):
"""
推出柱塞(排液)
推出柱塞(排液) 📤
Args:
volume (float): 要推出的体积 (ml)
@@ -207,35 +303,44 @@ class VirtualTransferPump:
actual_volume = self._position - new_position
if actual_volume <= 0:
self.logger.warning("Cannot push - already at minimum volume")
self.logger.warning("⚠️ 无法排液 - 已达到最小容量")
return
duration = self._calculate_duration(actual_volume, velocity)
display_duration = self._calculate_display_duration(actual_volume, velocity)
actual_duration = self._calculate_duration(actual_volume, velocity)
self.logger.info(f"Pushing {actual_volume} ml (from {self._position} to {new_position})")
self.logger.info(f"📤 开始排液: {actual_volume:.2f}mL")
self.logger.info(f" 📍 位置: {self._position:.2f}mL → {new_position:.2f}mL")
self.logger.info(f" ⏰ 预计时间: {display_duration:.2f}s")
await self._simulate_operation(duration)
if self._fast_mode:
self.logger.info(f" ⚡ 快速模式: 实际用时 {actual_duration:.2f}s")
await self._simulate_operation(actual_duration)
self._position = new_position
self._current_volume = new_position
self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml")
self.logger.info(f"✅ 排液完成: {actual_volume:.2f}mL | 💧 当前体积: {self._current_volume:.2f}mL")
# 便捷操作方法
async def aspirate(self, volume: float, velocity: float = None):
"""吸液操作"""
"""吸液操作 📥"""
await self.pull_plunger(volume, velocity)
async def dispense(self, volume: float, velocity: float = None):
"""排液操作"""
"""排液操作 📤"""
await self.push_plunger(volume, velocity)
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
"""转移操作(先吸后排)"""
"""转移操作(先吸后排) 🔄"""
self.logger.info(f"🔄 开始转移操作: {volume:.2f}mL")
# 吸液
await self.aspirate(volume, aspirate_velocity)
# 短暂停顿
self.logger.debug("⏸️ 短暂停顿...")
await asyncio.sleep(0.1)
# 排液

View File

@@ -100,3 +100,4 @@ serial:
properties: {}
required: []
type: object
version: 0.0.1

View File

@@ -8,7 +8,7 @@ camera:
handles: []
result: {}
schema:
description: destroy_node的参数schema
description: 用于安全地关闭摄像头设备释放摄像头资源停止视频采集和发布服务。调用此函数将清理OpenCV摄像头连接并销毁ROS2节点。
properties:
feedback: {}
goal:
@@ -28,7 +28,7 @@ camera:
handles: []
result: {}
schema:
description: timer_callback的参数schema
description: 定时器回调函数的参数schema。此函数负责定期采集摄像头视频帧将OpenCV格式的图像转换为ROS Image消息格式并发布到指定的视频话题。默认以10Hz频率执行确保视频流的连续性和实时性。
properties:
feedback: {}
goal:
@@ -44,7 +44,7 @@ camera:
module: unilabos.ros.nodes.presets.camera:VideoPublisher
status_types: {}
type: ros2
description: ''
description: VideoPublisher摄像头设备节点用于实时视频采集和流媒体发布。该设备通过OpenCV连接本地摄像头如USB摄像头、内置摄像头等定时采集视频帧并将其转换为ROS2的sensor_msgs/Image消息格式发布到视频话题。主要用于实验室自动化系统中的视觉监控、图像分析、实时观察等应用场景。支持可配置的摄像头索引、发布频率等参数。
handles: []
icon: ''
init_param_schema:
@@ -67,3 +67,4 @@ camera:
properties: {}
required: []
type: object
version: 0.0.1

View File

@@ -8,7 +8,7 @@ hplc.agilent:
handles: []
result: {}
schema:
description: check_status的参数schema
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态确保系统稳定运行及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
properties:
feedback: {}
goal:
@@ -29,7 +29,7 @@ hplc.agilent:
handles: []
result: {}
schema:
description: extract_data_from_txt的参数schema
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
properties:
feedback: {}
goal:
@@ -55,7 +55,7 @@ hplc.agilent:
handles: []
result: {}
schema:
description: start_sequence的参数schema
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
properties:
feedback: {}
goal:
@@ -83,7 +83,7 @@ hplc.agilent:
handles: []
result: {}
schema:
description: try_close_sub_device的参数schema
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
properties:
feedback: {}
goal:
@@ -106,7 +106,7 @@ hplc.agilent:
handles: []
result: {}
schema:
description: try_open_sub_device的参数schema
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块建立设备通信并进行自检。该函数提供连接验证和错误恢复机制确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
properties:
feedback: {}
goal:
@@ -131,10 +131,9 @@ hplc.agilent:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -143,7 +142,6 @@ hplc.agilent:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -152,7 +150,6 @@ hplc.agilent:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -179,7 +176,7 @@ hplc.agilent:
status_text: str
success: bool
type: python
description: HPLC device
description: 安捷伦高效液相色谱HPLC分析设备用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
handles: []
icon: ''
init_param_schema:
@@ -218,6 +215,7 @@ hplc.agilent:
- finish_status
- data_file
type: object
version: 0.0.1
raman_home_made:
class:
action_value_mappings:
@@ -229,7 +227,7 @@ raman_home_made:
handles: []
result: {}
schema:
description: ccd_time的参数schema
description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数优化测量效果。
properties:
feedback: {}
goal:
@@ -253,7 +251,7 @@ raman_home_made:
handles: []
result: {}
schema:
description: laser_on_power的参数schema
description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。
properties:
feedback: {}
goal:
@@ -269,30 +267,6 @@ raman_home_made:
title: laser_on_power参数
type: object
type: UniLabJsonCommand
auto-raman_cmd:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: raman_cmd的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: raman_cmd参数
type: object
type: UniLabJsonCommand
auto-raman_without_background:
feedback: {}
goal: {}
@@ -302,7 +276,7 @@ raman_home_made:
handles: []
result: {}
schema:
description: raman_without_background的参数schema
description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。
properties:
feedback: {}
goal:
@@ -332,7 +306,7 @@ raman_home_made:
handles: []
result: {}
schema:
description: raman_without_background_average的参数schema
description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。
properties:
feedback: {}
goal:
@@ -367,10 +341,9 @@ raman_home_made:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -379,7 +352,6 @@ raman_home_made:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -388,7 +360,6 @@ raman_home_made:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -407,7 +378,7 @@ raman_home_made:
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
status_types: {}
type: python
description: Raman spectroscopy device
description: 拉曼光谱分析设备用于物质的分子结构和化学成分表征。该设备集成激光器和CCD检测器通过串口通信控制激光功率和光谱采集。具备背景扣除、多次平均、自动数据处理等功能支持高精度的拉曼光谱测量。适用于材料表征、化学分析、质量控制、研究开发等需要分子指纹识别和结构分析的实验应用。
handles: []
icon: ''
init_param_schema:
@@ -431,3 +402,4 @@ raman_home_made:
properties: {}
required: []
type: object
version: 0.0.1

View File

@@ -5,7 +5,7 @@ hotel.thermo_orbitor_rs2_hotel:
status_types:
rotation: String
type: python
description: Thermo Orbitor RS2 Hotel
description: Thermo Orbitor RS2 Hotel容器设备用于实验室样品的存储和管理。该设备通过HotelContainer类实现容器的旋转控制和状态监控主要用于存储实验样品、试剂瓶或其他实验器具支持旋转功能以便于样品的自动化存取。适用于需要有序存储和快速访问大量样品的实验室自动化场景。
handles: []
icon: ''
init_param_schema:
@@ -29,3 +29,4 @@ hotel.thermo_orbitor_rs2_hotel:
model:
mesh: thermo_orbitor_rs2_hotel
type: device
version: 0.0.1

View File

@@ -15,16 +15,14 @@ laiyu_add_solid:
result:
actual_mass_mg: actual_mass_mg
schema:
description: ROS Action SolidDispenseAddPowderTube 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: SolidDispenseAddPowderTube_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
compound_mass:
type: number
@@ -41,7 +39,6 @@ laiyu_add_solid:
title: SolidDispenseAddPowderTube_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
actual_mass_mg:
type: number
@@ -60,38 +57,6 @@ laiyu_add_solid:
title: SolidDispenseAddPowderTube
type: object
type: SolidDispenseAddPowderTube
auto-add_powder_tube:
feedback: {}
goal: {}
goal_default:
compound_mass: null
powder_tube_number: null
target_tube_position: null
handles: []
result: {}
schema:
description: add_powder_tube的参数schema
properties:
feedback: {}
goal:
properties:
compound_mass:
type: string
powder_tube_number:
type: string
target_tube_position:
type: string
required:
- powder_tube_number
- target_tube_position
- compound_mass
type: object
result: {}
required:
- goal
title: add_powder_tube参数
type: object
type: UniLabJsonCommand
auto-calculate_crc:
feedback: {}
goal: {}
@@ -100,7 +65,7 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: calculate_crc的参数schema
description: Modbus CRC-16校验码计算函数。计算Modbus RTU通信协议所需的CRC-16校验码确保数据传输的完整性和可靠性。该函数实现标准的CRC-16算法用于构造完整的Modbus指令帧。
properties:
feedback: {}
goal:
@@ -116,154 +81,6 @@ laiyu_add_solid:
title: calculate_crc参数
type: object
type: UniLabJsonCommand
auto-discharge:
feedback: {}
goal: {}
goal_default:
float_in: null
handles: []
result: {}
schema:
description: discharge的参数schema
properties:
feedback: {}
goal:
properties:
float_in:
type: number
required:
- float_in
type: object
result: {}
required:
- goal
title: discharge参数
type: object
type: UniLabJsonCommand
auto-move_to_plate:
feedback: {}
goal: {}
goal_default:
string: null
handles: []
result: {}
schema:
description: move_to_plate的参数schema
properties:
feedback: {}
goal:
properties:
string:
type: string
required:
- string
type: object
result: {}
required:
- goal
title: move_to_plate参数
type: object
type: UniLabJsonCommand
auto-move_to_xyz:
feedback: {}
goal: {}
goal_default:
x: null
y: null
z: null
handles: []
result: {}
schema:
description: move_to_xyz的参数schema
properties:
feedback: {}
goal:
properties:
x:
type: number
y:
type: number
z:
type: number
required:
- x
- y
- z
type: object
result: {}
required:
- goal
title: move_to_xyz参数
type: object
type: UniLabJsonCommand
auto-pick_powder_tube:
feedback: {}
goal: {}
goal_default:
int_input: null
handles: []
result: {}
schema:
description: pick_powder_tube的参数schema
properties:
feedback: {}
goal:
properties:
int_input:
type: integer
required:
- int_input
type: object
result: {}
required:
- goal
title: pick_powder_tube参数
type: object
type: UniLabJsonCommand
auto-put_powder_tube:
feedback: {}
goal: {}
goal_default:
int_input: null
handles: []
result: {}
schema:
description: put_powder_tube的参数schema
properties:
feedback: {}
goal:
properties:
int_input:
type: integer
required:
- int_input
type: object
result: {}
required:
- goal
title: put_powder_tube参数
type: object
type: UniLabJsonCommand
auto-reset:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: reset的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: reset参数
type: object
type: UniLabJsonCommand
auto-send_command:
feedback: {}
goal: {}
@@ -272,7 +89,7 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: send_command的参数schema
description: Modbus指令发送函数。构造完整的Modbus RTU指令帧包含CRC校验发送给分装设备并等待响应。该函数处理底层通信协议确保指令的正确传输和响应接收支持最长3分钟的响应等待时间。
properties:
feedback: {}
goal:
@@ -297,16 +114,14 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: ROS Action FloatSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: FloatSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
float_in:
type: number
@@ -315,7 +130,6 @@ laiyu_add_solid:
title: FloatSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -340,16 +154,14 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: ROS Action StrSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: StrSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
string:
type: string
@@ -358,7 +170,6 @@ laiyu_add_solid:
title: StrSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -387,16 +198,14 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: ROS Action Point3DSeparateInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: Point3DSeparateInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
x:
type: number
@@ -411,7 +220,6 @@ laiyu_add_solid:
title: Point3DSeparateInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -436,16 +244,14 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: ROS Action IntSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: IntSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
int_input:
maximum: 2147483647
@@ -456,7 +262,6 @@ laiyu_add_solid:
title: IntSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -481,16 +286,14 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: ROS Action IntSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: IntSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
int_input:
maximum: 2147483647
@@ -501,7 +304,6 @@ laiyu_add_solid:
title: IntSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -524,22 +326,19 @@ laiyu_add_solid:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -556,7 +355,7 @@ laiyu_add_solid:
status_types:
status: str
type: python
description: Laiyu Add Solid
description: 来渝固体粉末自动分装设备用于实验室化学试剂的精确称量和分装。该设备通过Modbus RTU协议与控制系统通信集成了精密天平、三轴运动平台、粉筒管理系统等组件。支持多种粉末试剂的自动拿取、精确称量、定点分装和归位操作。具备高精度称量、位置控制和批量处理能力适用于化学合成、药物研发、材料制备等需要精确固体试剂配制的实验室应用场景。
handles: []
icon: ''
init_param_schema:
@@ -580,3 +379,4 @@ laiyu_add_solid:
required:
- status
type: object
version: 0.0.1

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -117,30 +117,6 @@ moveit.arm_slider:
title: moveit_task参数
type: object
type: UniLabJsonCommand
auto-pick_and_place:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: pick_and_place的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: pick_and_place参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
@@ -193,54 +169,6 @@ moveit.arm_slider:
title: resource_manager参数
type: object
type: UniLabJsonCommand
auto-set_position:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: set_position的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: set_position参数
type: object
type: UniLabJsonCommand
auto-set_status:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: set_status的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: set_status参数
type: object
type: UniLabJsonCommand
auto-wait_for_resource_action:
feedback: {}
goal: {}
@@ -270,10 +198,9 @@ moveit.arm_slider:
handles: []
result: {}
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -282,7 +209,6 @@ moveit.arm_slider:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -291,7 +217,6 @@ moveit.arm_slider:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -316,10 +241,9 @@ moveit.arm_slider:
handles: []
result: {}
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -328,7 +252,6 @@ moveit.arm_slider:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -337,7 +260,6 @@ moveit.arm_slider:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -362,10 +284,9 @@ moveit.arm_slider:
handles: []
result: {}
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -374,7 +295,6 @@ moveit.arm_slider:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -383,7 +303,6 @@ moveit.arm_slider:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -402,7 +321,7 @@ moveit.arm_slider:
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
status_types: {}
type: python
description: Arm with Slider
description: 机械臂与滑块运动系统基于MoveIt2运动规划框架的多自由度机械臂控制设备。该系统集成机械臂和线性滑块通过ROS2和MoveIt2实现精确的轨迹规划和协调运动控制。支持笛卡尔空间和关节空间的运动规划、碰撞检测、逆运动学求解等功能。适用于复杂的pick-and-place操作、精密装配、多工位协作等需要高精度多轴协调运动的实验室自动化应用。
handles: []
icon: ''
init_param_schema:
@@ -427,6 +346,7 @@ moveit.arm_slider:
model:
mesh: arm_slider
type: device
version: 0.0.1
moveit.toyo_xyz:
class:
action_value_mappings:
@@ -546,30 +466,6 @@ moveit.toyo_xyz:
title: moveit_task参数
type: object
type: UniLabJsonCommand
auto-pick_and_place:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: pick_and_place的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: pick_and_place参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
@@ -622,54 +518,6 @@ moveit.toyo_xyz:
title: resource_manager参数
type: object
type: UniLabJsonCommand
auto-set_position:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: set_position的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: set_position参数
type: object
type: UniLabJsonCommand
auto-set_status:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: set_status的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: set_status参数
type: object
type: UniLabJsonCommand
auto-wait_for_resource_action:
feedback: {}
goal: {}
@@ -699,10 +547,9 @@ moveit.toyo_xyz:
handles: []
result: {}
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -711,7 +558,6 @@ moveit.toyo_xyz:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -720,7 +566,6 @@ moveit.toyo_xyz:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -745,10 +590,9 @@ moveit.toyo_xyz:
handles: []
result: {}
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -757,7 +601,6 @@ moveit.toyo_xyz:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -766,7 +609,6 @@ moveit.toyo_xyz:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -791,10 +633,9 @@ moveit.toyo_xyz:
handles: []
result: {}
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -803,7 +644,6 @@ moveit.toyo_xyz:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -812,7 +652,6 @@ moveit.toyo_xyz:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -831,7 +670,7 @@ moveit.toyo_xyz:
module: unilabos.devices.ros_dev.moveit_interface:MoveitInterface
status_types: {}
type: python
description: Toyo XYZ
description: 东洋XYZ三轴运动平台基于MoveIt2运动规划框架的精密定位设备。该设备通过ROS2和MoveIt2实现三维空间的精确运动控制支持复杂轨迹规划、多点定位、速度控制等功能。具备高精度定位、平稳运动、实时轨迹监控等特性。适用于精密加工、样品定位、检测扫描、自动化装配等需要高精度三维运动控制的实验室和工业应用场景。
handles: []
icon: ''
init_param_schema:
@@ -856,3 +695,4 @@ moveit.toyo_xyz:
model:
mesh: toyo_xyz
type: device
version: 0.0.1

View File

@@ -93,30 +93,6 @@ rotavap.one:
title: set_rotate_time参数
type: object
type: UniLabJsonCommand
auto-set_timer:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: set_timer的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: set_timer参数
type: object
type: UniLabJsonCommand
set_timer:
feedback: {}
goal:
@@ -127,10 +103,9 @@ rotavap.one:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -139,7 +114,6 @@ rotavap.one:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -148,7 +122,6 @@ rotavap.one:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -167,7 +140,7 @@ rotavap.one:
module: unilabos.devices.rotavap.rotavap_one:RotavapOne
status_types: {}
type: python
description: Rotavap device
description: 旋转蒸发仪设备,用于有机化学实验中的溶剂回收和浓缩操作。该设备通过串口通信控制,集成旋转和真空泵功能,支持定时控制和自动化操作。具备旋转速度调节、真空度控制、温度管理等功能,实现高效的溶剂蒸发和回收。适用于有机合成、天然产物提取、药物制备等需要溶剂去除和浓缩的实验室应用场景。
handles: []
icon: ''
init_param_schema:
@@ -185,6 +158,7 @@ rotavap.one:
properties: {}
required: []
type: object
version: 0.0.1
separator.homemade:
class:
action_value_mappings:
@@ -208,38 +182,6 @@ separator.homemade:
title: read_sensor_loop参数
type: object
type: UniLabJsonCommand
auto-stir:
feedback: {}
goal: {}
goal_default:
settling_time: 10
stir_speed: 300
stir_time: 10
handles: []
result: {}
schema:
description: stir的参数schema
properties:
feedback: {}
goal:
properties:
settling_time:
default: 10
type: number
stir_speed:
default: 300
type: number
stir_time:
default: 10
type: number
required: []
type: object
result: {}
required:
- goal
title: stir参数
type: object
type: UniLabJsonCommand
auto-valve_open:
feedback: {}
goal: {}
@@ -268,30 +210,6 @@ separator.homemade:
title: valve_open参数
type: object
type: UniLabJsonCommand
auto-valve_open_cmd:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: valve_open_cmd的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: valve_open_cmd参数
type: object
type: UniLabJsonCommand
auto-write:
feedback: {}
goal: {}
@@ -335,10 +253,9 @@ separator.homemade:
result:
success: success
schema:
description: ROS Action Stir 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -347,7 +264,6 @@ separator.homemade:
title: Stir_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
event:
type: string
@@ -374,7 +290,6 @@ separator.homemade:
title: Stir_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -401,13 +316,12 @@ separator.homemade:
goal_default:
command: ''
handles: []
result":
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -416,7 +330,6 @@ separator.homemade:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -425,7 +338,6 @@ separator.homemade:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -444,7 +356,7 @@ separator.homemade:
module: unilabos.devices.separator.homemade_grbl_conductivity:SeparatorController
status_types: {}
type: python
description: Separator device with homemade grbl controller
description: 液-液分离器设备基于自制Grbl控制器的自动化分离系统。该设备集成搅拌、沉降、阀门控制和电导率传感器通过串口通信实现精确的分离操作控制。支持自动搅拌、分层沉降、基于传感器反馈的智能分液等功能。适用于有机化学中的萃取分离、相分离、液-液提取等需要精确分离控制的实验应用。
handles: []
icon: ''
init_param_schema:
@@ -468,3 +380,4 @@ separator.homemade:
properties: {}
required: []
type: object
version: 0.0.1

View File

@@ -125,30 +125,6 @@ solenoid_valve:
title: send_command参数
type: object
type: UniLabJsonCommand
auto-set_valve_position:
feedback: {}
goal: {}
goal_default:
position: null
handles: []
result: {}
schema:
description: set_valve_position的参数schema
properties:
feedback: {}
goal:
properties:
position:
type: string
required:
- position
type: object
result: {}
required:
- goal
title: set_valve_position参数
type: object
type: UniLabJsonCommand
set_valve_position:
feedback: {}
goal:
@@ -158,16 +134,14 @@ solenoid_valve:
handles: []
result: {}
schema:
description: ROS Action StrSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: StrSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
string:
type: string
@@ -176,7 +150,6 @@ solenoid_valve:
title: StrSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -197,7 +170,7 @@ solenoid_valve:
status: str
valve_position: str
type: python
description: Solenoid valve
description: 电磁阀控制设备,用于精确的流体路径控制和开关操作。该设备通过串口通信控制电磁阀的开关状态,支持远程操作和状态监测。具备快速响应、可靠密封、状态反馈等特性,广泛应用于流体输送、样品进样、路径切换等需要精确流体控制的实验室自动化应用。
handles: []
icon: ''
init_param_schema:
@@ -218,29 +191,10 @@ solenoid_valve:
- status
- valve_position
type: object
version: 0.0.1
solenoid_valve.mock:
class:
action_value_mappings:
auto-close:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: close的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: close参数
type: object
type: UniLabJsonCommand
auto-is_closed:
feedback: {}
goal: {}
@@ -281,26 +235,6 @@ solenoid_valve.mock:
title: is_open参数
type: object
type: UniLabJsonCommand
auto-open:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: open的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: open参数
type: object
type: UniLabJsonCommand
auto-set_valve_position:
feedback: {}
goal: {}
@@ -332,22 +266,19 @@ solenoid_valve.mock:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -367,22 +298,19 @@ solenoid_valve.mock:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -400,7 +328,7 @@ solenoid_valve.mock:
status: str
valve_position: str
type: python
description: Mock solenoid valve
description: 模拟电磁阀设备,用于系统测试和开发调试。该设备模拟真实电磁阀的开关操作和状态变化,提供与实际设备相同的控制接口和反馈机制。支持流体路径的虚拟控制,便于在没有实际硬件的情况下进行流体系统的集成测试和算法验证。适用于系统开发、流程调试和培训演示等场景。
handles:
- data_type: fluid
handler_key: in
@@ -431,6 +359,7 @@ solenoid_valve.mock:
- status
- valve_position
type: object
version: 0.0.1
syringe_pump_with_valve.runze:
class:
action_value_mappings:
@@ -825,7 +754,7 @@ syringe_pump_with_valve.runze:
velocity_grade: String
velocity_init: String
type: python
description: Runze Syringe pump with valve
description: 润泽精密注射泵设备,集成阀门控制的高精度流体输送系统。该设备通过串口通信控制,支持多种运行模式和精确的体积控制。具备可变速度控制、精密定位、阀门切换、实时状态监控等功能。适用于微量液体输送、精密进样、流速控制、化学反应进料等需要高精度流体操作的实验室自动化应用。
handles: []
icon: ''
init_param_schema:
@@ -875,3 +804,4 @@ syringe_pump_with_valve.runze:
- position
- plunger_position
type: object
version: 0.0.1

View File

@@ -11,7 +11,7 @@ agv.SEER:
handles: []
result: {}
schema:
description: send的参数schema
description: AGV底层通信命令发送函数。通过TCP socket连接向AGV发送底层控制命令支持pose位置、status状态、nav导航等命令类型。用于获取AGV当前位置坐标、运行状态或发送导航指令。该函数封装了AGV的通信协议将命令转换为十六进制数据包并处理响应解析。
properties:
feedback: {}
goal:
@@ -33,30 +33,6 @@ agv.SEER:
title: send参数
type: object
type: UniLabJsonCommand
auto-send_nav_task:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: send_nav_task的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: send_nav_task参数
type: object
type: UniLabJsonCommand
send_nav_task:
feedback: {}
goal:
@@ -67,10 +43,9 @@ agv.SEER:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -79,7 +54,6 @@ agv.SEER:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -88,7 +62,6 @@ agv.SEER:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -109,7 +82,7 @@ agv.SEER:
pose: list
status: str
type: python
description: SEER AGV
description: SEER AGV自动导引车设备用于实验室内物料和设备的自主移动运输。该AGV通过TCP socket与导航系统通信具备精确的定位和路径规划能力。支持实时位置监控、状态查询和导航任务执行可在预设的实验室环境中自主移动至指定位置。适用于样品运输、设备转移、多工位协作等实验室自动化物流场景。
handles: []
icon: ''
init_param_schema:
@@ -130,3 +103,4 @@ agv.SEER:
- pose
- status
type: object
version: 0.0.1

View File

@@ -8,7 +8,7 @@ robotic_arm.UR:
handles: []
result: {}
schema:
description: arm_init的参数schema
description: 机械臂初始化函数。执行UR机械臂的完整初始化流程包括上电、释放制动器、解除保护停止状态等。该函数确保机械臂从安全停止状态恢复到可操作状态是机械臂使用前的必要步骤。初始化完成后机械臂将处于就绪状态可以接收后续的运动指令。
properties:
feedback: {}
goal:
@@ -29,7 +29,7 @@ robotic_arm.UR:
handles: []
result: {}
schema:
description: load_pose_data的参数schema
description: 从JSON字符串加载位置数据函数。接收包含机械臂位置信息的JSON格式字符串解析并存储位置数据供后续运动任务使用。位置数据通常包含多个预定义的工作位置坐标用于实现精确的多点运动控制。适用于动态配置机械臂工作位置的场景。
properties:
feedback: {}
goal:
@@ -53,7 +53,7 @@ robotic_arm.UR:
handles: []
result: {}
schema:
description: load_pose_file的参数schema
description: 从文件加载位置数据函数。读取指定的JSON文件并加载其中的机械臂位置信息。该函数支持从外部配置文件中获取预设的工作位置便于位置数据的管理和重用。适用于需要从固定配置文件中读取复杂位置序列的应用场景。
properties:
feedback: {}
goal:
@@ -69,30 +69,6 @@ robotic_arm.UR:
title: load_pose_file参数
type: object
type: UniLabJsonCommand
auto-move_pos_task:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: move_pos_task的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: move_pos_task参数
type: object
type: UniLabJsonCommand
auto-reload_pose:
feedback: {}
goal: {}
@@ -100,7 +76,7 @@ robotic_arm.UR:
handles: []
result: {}
schema:
description: reload_pose的参数schema
description: 重新加载位置数据函数。重新读取并解析之前设置的位置文件,更新内存中的位置数据。该函数用于在位置文件被修改后刷新机械臂的位置配置,无需重新初始化整个系统。适用于动态更新机械臂工作位置的场景。
properties:
feedback: {}
goal:
@@ -123,10 +99,9 @@ robotic_arm.UR:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -135,7 +110,6 @@ robotic_arm.UR:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -144,7 +118,6 @@ robotic_arm.UR:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -167,7 +140,7 @@ robotic_arm.UR:
gripper_pose: float
gripper_status: str
type: python
description: UR robotic arm
description: Universal Robots机械臂设备用于实验室精密操作和自动化作业。该设备集成了UR机械臂本体、Robotiq夹爪和RTDE通信接口支持六自由度精确运动控制和力觉反馈。具备实时位置监控、状态反馈、轨迹规划等功能可执行复杂的多点位运动任务。适用于样品抓取、精密装配、实验器具操作等需要高精度和高重复性的实验室自动化场景。
handles: []
icon: ''
init_param_schema:
@@ -197,3 +170,4 @@ robotic_arm.UR:
- arm_status
- gripper_status
type: object
version: 0.0.1

View File

@@ -51,7 +51,7 @@ gripper.misumi_rz:
handles: []
result: {}
schema:
description: gripper_move的参数schema
description: 夹爪抓取运动控制函数。控制夹爪的开合运动,支持位置、速度、力矩的精确设定。位置参数控制夹爪开合程度,速度参数控制运动快慢,力矩参数控制夹持强度。该函数提供安全的力控制,避免损坏被抓取物体,适用于各种形状和材质的物品抓取。
properties:
feedback: {}
goal:
@@ -80,7 +80,7 @@ gripper.misumi_rz:
handles: []
result: {}
schema:
description: init_gripper的参数schema
description: 夹爪初始化函数。执行Misumi RZ夹爪的完整初始化流程包括Modbus通信建立、电机参数配置、传感器校准等。该函数确保夹爪系统从安全状态恢复到可操作状态是夹爪使用前的必要步骤。初始化完成后夹爪将处于就绪状态可接收抓取和旋转指令。
properties:
feedback: {}
goal:
@@ -169,7 +169,7 @@ gripper.misumi_rz:
handles: []
result: {}
schema:
description: node_gripper_move的参数schema
description: 节点夹爪移动任务函数。接收逗号分隔的命令字符串,解析位置、速度、力矩参数并执行夹爪抓取动作。该函数等待运动完成并返回执行结果,提供同步的运动控制接口。适用于需要可靠完成确认的精密抓取操作。
properties:
feedback: {}
goal:
@@ -193,7 +193,7 @@ gripper.misumi_rz:
handles: []
result: {}
schema:
description: node_rotate_move的参数schema
description: 节点旋转移动任务函数。接收逗号分隔的命令字符串,解析角度、速度、力矩参数并执行夹爪旋转动作。该函数等待旋转完成并返回执行结果,提供同步的旋转控制接口。适用于需要精确角度定位和完成确认的旋转操作。
properties:
feedback: {}
goal:
@@ -251,7 +251,7 @@ gripper.misumi_rz:
handles: []
result: {}
schema:
description: rotate_move_abs的参数schema
description: 夹爪绝对位置旋转控制函数。控制夹爪主轴旋转到指定的绝对角度位置支持360度连续旋转。位置参数指定目标角度速度参数控制旋转速率力矩参数设定旋转阻力限制。该函数提供高精度的角度定位适用于需要精确方向控制的操作场景。
properties:
feedback: {}
goal:
@@ -379,10 +379,9 @@ gripper.misumi_rz:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -391,7 +390,6 @@ gripper.misumi_rz:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -400,7 +398,6 @@ gripper.misumi_rz:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -420,7 +417,7 @@ gripper.misumi_rz:
status_types:
status: str
type: python
description: Misumi RZ gripper
description: Misumi RZ系列电子夹爪设备集成旋转和抓取双重功能的精密夹爪系统。该设备通过Modbus RTU协议与控制系统通信支持位置、速度、力矩的精确控制。具备高精度的位置反馈、实时状态监控和故障检测功能。适用于需要精密抓取和旋转操作的实验室自动化场景如样品管理、精密装配、器件操作等应用。
handles: []
icon: ''
init_param_schema:
@@ -447,6 +444,7 @@ gripper.misumi_rz:
required:
- status
type: object
version: 0.0.1
gripper.mock:
class:
action_value_mappings:
@@ -461,7 +459,7 @@ gripper.mock:
handles: []
result: {}
schema:
description: edit_id的参数schema
description: 模拟夹爪资源ID编辑函数。用于测试和演示资源管理功能模拟修改夹爪资源的标识信息。该函数接收工作流名称、参数和资源对象模拟真实的资源更新过程并返回修改后的资源信息。适用于系统测试和开发调试场景。
properties:
feedback: {}
goal:
@@ -484,38 +482,6 @@ gripper.mock:
title: edit_id参数
type: object
type: UniLabJsonCommand
auto-push_to:
feedback: {}
goal: {}
goal_default:
position: null
torque: null
velocity: 0.0
handles: []
result: {}
schema:
description: push_to的参数schema
properties:
feedback: {}
goal:
properties:
position:
type: number
torque:
type: number
velocity:
default: 0.0
type: number
required:
- position
- torque
type: object
result: {}
required:
- goal
title: push_to参数
type: object
type: UniLabJsonCommand
push_to:
feedback:
effort: torque
@@ -532,10 +498,9 @@ gripper.mock:
effort: torque
position: position
schema:
description: ROS Action GripperCommand 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
effort:
type: number
@@ -553,7 +518,6 @@ gripper.mock:
title: GripperCommand_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
properties:
@@ -571,7 +535,6 @@ gripper.mock:
title: GripperCommand_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
effort:
type: number
@@ -600,7 +563,7 @@ gripper.mock:
torque: float
velocity: float
type: python
description: Mock gripper
description: 模拟夹爪设备,用于系统测试和开发调试。该设备模拟真实夹爪的位置、速度、力矩等物理特性,支持虚拟的抓取和移动操作。提供与真实夹爪相同的接口和状态反馈,便于在没有实际硬件的情况下进行系统集成测试和算法验证。适用于软件开发、系统调试和培训演示等场景。
handles: []
icon: ''
init_param_schema:
@@ -624,3 +587,4 @@ gripper.mock:
- torque
- status
type: object
version: 0.0.1

View File

@@ -8,7 +8,7 @@ linear_motion.grbl:
handles: []
result: {}
schema:
description: initialize的参数schema
description: CNC设备初始化函数。执行Grbl CNC的完整初始化流程包括归零操作、轴校准和状态复位。该函数将所有轴移动到原点位置(0,0,0),确保设备处于已知的参考状态。初始化完成后设备进入空闲状态,可接收后续的运动指令。
properties:
feedback: {}
goal:
@@ -21,30 +21,6 @@ linear_motion.grbl:
title: initialize参数
type: object
type: UniLabJsonCommand
auto-move_through_points:
feedback: {}
goal: {}
goal_default:
positions: null
handles: []
result: {}
schema:
description: move_through_points的参数schema
properties:
feedback: {}
goal:
properties:
positions:
type: array
required:
- positions
type: object
result: {}
required:
- goal
title: move_through_points参数
type: object
type: UniLabJsonCommand
auto-set_position:
feedback: {}
goal: {}
@@ -53,7 +29,7 @@ linear_motion.grbl:
handles: []
result: {}
schema:
description: set_position的参数schema
description: CNC绝对位置设定函数。控制CNC设备移动到指定的三维坐标位置(x,y,z)。该函数支持安全限位检查,防止超出设备工作范围。移动过程中会监控设备状态,确保安全到达目标位置。适用于精确定位和轨迹控制操作。
properties:
feedback: {}
goal:
@@ -69,34 +45,6 @@ linear_motion.grbl:
title: set_position参数
type: object
type: UniLabJsonCommand
auto-set_spindle_speed:
feedback: {}
goal: {}
goal_default:
max_velocity: 500
spindle_speed: null
handles: []
result: {}
schema:
description: set_spindle_speed的参数schema
properties:
feedback: {}
goal:
properties:
max_velocity:
default: 500
type: number
spindle_speed:
type: number
required:
- spindle_speed
type: object
result: {}
required:
- goal
title: set_spindle_speed参数
type: object
type: UniLabJsonCommand
auto-stop_operation:
feedback: {}
goal: {}
@@ -104,7 +52,7 @@ linear_motion.grbl:
handles: []
result: {}
schema:
description: stop_operation的参数schema
description: CNC操作停止函数。立即停止当前正在执行的所有CNC运动包括轴移动和主轴旋转。该函数用于紧急停止或任务中断确保设备和工件的安全。停止后设备将保持当前位置等待新的指令。
properties:
feedback: {}
goal:
@@ -166,10 +114,9 @@ linear_motion.grbl:
handles: []
result: {}
schema:
description: ROS Action NavigateThroughPoses 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
current_pose:
properties:
@@ -290,7 +237,6 @@ linear_motion.grbl:
title: NavigateThroughPoses_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
behavior_tree:
type: string
@@ -371,7 +317,6 @@ linear_motion.grbl:
title: NavigateThroughPoses_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
result:
properties: {}
@@ -401,10 +346,9 @@ linear_motion.grbl:
handles: []
result: {}
schema:
description: ROS Action SingleJointPosition 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
error:
type: number
@@ -444,7 +388,6 @@ linear_motion.grbl:
title: SingleJointPosition_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
max_velocity:
type: number
@@ -472,7 +415,6 @@ linear_motion.grbl:
title: SingleJointPosition_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties: {}
required: []
title: SingleJointPosition_Result
@@ -488,7 +430,7 @@ linear_motion.grbl:
spindle_speed: float
status: str
type: python
description: Grbl CNC
description: Grbl数控机床CNC设备用于实验室精密加工和三轴定位操作。该设备基于Grbl固件通过串口通信控制步进电机实现X、Y、Z三轴的精确运动。支持绝对定位、轨迹规划、主轴控制和实时状态监控。具备安全限位保护和运动平滑控制功能。适用于精密钻孔、铣削、雕刻、样品制备等需要高精度定位和加工的实验室应用场景。
handles: []
icon: ''
init_param_schema:
@@ -524,6 +466,7 @@ linear_motion.grbl:
- position
- spindle_speed
type: object
version: 0.0.1
motor.iCL42:
class:
action_value_mappings:
@@ -537,7 +480,7 @@ motor.iCL42:
handles: []
result: {}
schema:
description: execute_run_motor的参数schema
description: 步进电机执行运动函数。直接执行电机运动命令,包括位置设定、速度控制和路径规划。该函数处理底层的电机控制协议,消除警告信息,设置运动参数并启动电机运行。适用于需要直接控制电机运动的应用场景。
properties:
feedback: {}
goal:
@@ -566,7 +509,7 @@ motor.iCL42:
handles: []
result: {}
schema:
description: init_device的参数schema
description: iCL42电机设备初始化函数。建立与iCL42步进电机驱动器的串口通信连接配置通信参数包括波特率、数据位、校验位等。该函数是电机使用前的必要步骤确保驱动器处于可控状态并准备接收运动指令。
properties:
feedback: {}
goal:
@@ -589,7 +532,7 @@ motor.iCL42:
handles: []
result: {}
schema:
description: run_motor的参数schema
description: 步进电机运动控制函数。根据指定的运动模式、目标位置和速度参数控制电机运动。支持多种运动模式和精确的位置控制,自动处理运动轨迹规划和执行。该函数提供异步执行和状态反馈,确保运动的准确性和可靠性。
properties:
feedback: {}
goal:
@@ -621,10 +564,9 @@ motor.iCL42:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -633,7 +575,6 @@ motor.iCL42:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -642,7 +583,6 @@ motor.iCL42:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -664,7 +604,7 @@ motor.iCL42:
motor_position: int
success: bool
type: python
description: iCL42 motor
description: iCL42步进电机驱动器用于实验室设备的精密线性运动控制。该设备通过串口通信控制iCL42型步进电机驱动器支持多种运动模式和精确的位置、速度控制。具备位置反馈、运行状态监控和故障检测功能。适用于自动进样器、样品传送、精密定位平台等需要准确线性运动控制的实验室自动化设备。
handles: []
icon: ''
init_param_schema:
@@ -691,3 +631,4 @@ motor.iCL42:
- is_executing_run
- success
type: object
version: 0.0.1

View File

@@ -285,7 +285,7 @@ lh_joint_publisher:
module: unilabos.devices.ros_dev.liquid_handler_joint_publisher:LiquidHandlerJointPublisher
status_types: {}
type: ros2
description: ''
description: 液体处理器关节发布器用于ROS2仿真系统中的液体处理设备运动控制。该节点通过发布关节状态驱动仿真模型中的机械臂运动支持三维坐标到关节空间的逆运动学转换、多关节协调控制、资源跟踪和TF变换。具备精确的位置控制、速度调节、pick-and-place操作等功能。适用于液体处理系统的虚拟仿真、运动规划验证、系统集成测试等应用场景。
handles: []
icon: ''
init_param_schema:
@@ -309,3 +309,4 @@ lh_joint_publisher:
properties: {}
required: []
type: object
version: 0.0.1

View File

@@ -89,30 +89,6 @@ chiller:
title: modbus_crc参数
type: object
type: UniLabJsonCommand
auto-set_temperature:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: set_temperature的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: set_temperature参数
type: object
type: UniLabJsonCommand
auto-stop:
feedback: {}
goal: {}
@@ -143,10 +119,9 @@ chiller:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -155,7 +130,6 @@ chiller:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -164,7 +138,6 @@ chiller:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -183,7 +156,7 @@ chiller:
module: unilabos.devices.temperature.chiller:Chiller
status_types: {}
type: python
description: Chiller
description: 实验室制冷设备用于精确的温度控制和冷却操作。该设备通过Modbus RTU协议与控制系统通信支持精确的温度设定和监控。具备快速降温、恒温控制和温度保持功能广泛应用于需要低温环境的化学反应、样品保存、结晶操作等实验场景。提供稳定可靠的冷却性能确保实验过程的温度精度。
handles: []
icon: ''
init_param_schema:
@@ -201,6 +174,7 @@ chiller:
properties: {}
required: []
type: object
version: 0.0.1
heaterstirrer.dalong:
class:
action_value_mappings:
@@ -224,50 +198,6 @@ heaterstirrer.dalong:
title: close参数
type: object
type: UniLabJsonCommand
auto-heatchill:
feedback: {}
goal: {}
goal_default:
purpose: reaction
stir: true
stir_speed: 300
temp: null
time: 3600
vessel: null
handles: []
result: {}
schema:
description: heatchill的参数schema
properties:
feedback: {}
goal:
properties:
purpose:
default: reaction
type: string
stir:
default: true
type: boolean
stir_speed:
default: 300
type: number
temp:
type: number
time:
default: 3600
type: number
vessel:
type: string
required:
- vessel
- temp
type: object
result: {}
required:
- goal
title: heatchill参数
type: object
type: UniLabJsonCommand
auto-set_stir_speed:
feedback: {}
goal: {}
@@ -320,54 +250,6 @@ heaterstirrer.dalong:
title: set_temp_inner参数
type: object
type: UniLabJsonCommand
auto-set_temp_target:
feedback: {}
goal: {}
goal_default:
temp: null
handles: []
result: {}
schema:
description: set_temp_target的参数schema
properties:
feedback: {}
goal:
properties:
temp:
type: string
required:
- temp
type: object
result: {}
required:
- goal
title: set_temp_target参数
type: object
type: UniLabJsonCommand
auto-set_temp_warning:
feedback: {}
goal: {}
goal_default:
temp: null
handles: []
result: {}
schema:
description: set_temp_warning的参数schema
properties:
feedback: {}
goal:
properties:
temp:
type: string
required:
- temp
type: object
result: {}
required:
- goal
title: set_temp_warning参数
type: object
type: UniLabJsonCommand
heatchill:
feedback:
status: status
@@ -391,10 +273,9 @@ heaterstirrer.dalong:
result:
success: success
schema:
description: ROS Action HeatChill 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -403,7 +284,6 @@ heaterstirrer.dalong:
title: HeatChill_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
pressure:
type: string
@@ -439,7 +319,6 @@ heaterstirrer.dalong:
title: HeatChill_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
message:
type: string
@@ -468,10 +347,9 @@ heaterstirrer.dalong:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -480,7 +358,6 @@ heaterstirrer.dalong:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -489,7 +366,6 @@ heaterstirrer.dalong:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -515,10 +391,9 @@ heaterstirrer.dalong:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -527,7 +402,6 @@ heaterstirrer.dalong:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -536,7 +410,6 @@ heaterstirrer.dalong:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -560,7 +433,7 @@ heaterstirrer.dalong:
temp_target: float
temp_warning: float
type: python
description: DaLong heater stirrer
description: 大龙加热搅拌器,集成加热和搅拌双重功能的实验室设备。该设备通过串口通信控制,支持精确的温度调节、搅拌速度控制和安全保护功能。具备实时温度监测、目标温度设定、安全温度报警等特性。适用于化学合成、样品制备、反应控制等需要同时进行加热和搅拌的实验操作,提供稳定均匀的反应环境。
handles: []
icon: ''
init_param_schema:
@@ -596,6 +469,7 @@ heaterstirrer.dalong:
- temp_warning
- temp_target
type: object
version: 0.0.1
tempsensor:
class:
action_value_mappings:
@@ -707,30 +581,6 @@ tempsensor:
title: send_prototype_command参数
type: object
type: UniLabJsonCommand
auto-set_warning:
feedback: {}
goal: {}
goal_default:
command: null
handles: []
result: {}
schema:
description: set_warning的参数schema
properties:
feedback: {}
goal:
properties:
command:
type: string
required:
- command
type: object
result: {}
required:
- goal
title: set_warning参数
type: object
type: UniLabJsonCommand
set_warning:
feedback: {}
goal:
@@ -741,10 +591,9 @@ tempsensor:
result:
success: success
schema:
description: ROS Action SendCmd 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties:
status:
type: string
@@ -753,7 +602,6 @@ tempsensor:
title: SendCmd_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
command:
type: string
@@ -762,7 +610,6 @@ tempsensor:
title: SendCmd_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -782,7 +629,7 @@ tempsensor:
status_types:
value: float
type: python
description: Temperature sensor
description: 高精度温度传感器设备用于实验室环境和设备的温度监测。该传感器通过Modbus RTU协议与控制系统通信提供实时准确的温度数据。具备高精度测量、报警温度设定、数据稳定性好等特点。适用于反应器监控、环境温度监测、设备保护等需要精确温度测量的实验场景为实验安全和数据可靠性提供保障。
handles: []
icon: ''
init_param_schema:
@@ -809,3 +656,4 @@ tempsensor:
required:
- value
type: object
version: 0.0.1

View File

@@ -1,26 +1,6 @@
gas_source.mock:
class:
action_value_mappings:
auto-close:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: close的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: close参数
type: object
type: UniLabJsonCommand
auto-is_closed:
feedback: {}
goal: {}
@@ -61,50 +41,6 @@ gas_source.mock:
title: is_open参数
type: object
type: UniLabJsonCommand
auto-open:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: open的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: open参数
type: object
type: UniLabJsonCommand
auto-set_status:
feedback: {}
goal: {}
goal_default:
string: null
handles: []
result: {}
schema:
description: set_status的参数schema
properties:
feedback: {}
goal:
properties:
string:
type: string
required:
- string
type: object
result: {}
required:
- goal
title: set_status参数
type: object
type: UniLabJsonCommand
close:
feedback: {}
goal: {}
@@ -112,22 +48,19 @@ gas_source.mock:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -147,22 +80,19 @@ gas_source.mock:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -184,16 +114,14 @@ gas_source.mock:
handles: []
result: {}
schema:
description: ROS Action StrSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: StrSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
string:
type: string
@@ -202,7 +130,6 @@ gas_source.mock:
title: StrSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -222,7 +149,7 @@ gas_source.mock:
status_types:
status: str
type: python
description: Mock gas source
description: 模拟气体源设备,用于系统测试和开发调试。该设备模拟真实气体源的开关控制和状态监测功能,支持气体供应的启停操作。提供与真实气体源相同的接口和状态反馈,便于在没有实际硬件的情况下进行系统集成测试和算法验证。适用于气路系统调试、软件开发和实验流程验证等场景。
handles:
- data_key: fluid_out
data_source: executor
@@ -246,29 +173,10 @@ gas_source.mock:
required:
- status
type: object
version: 0.0.1
vacuum_pump.mock:
class:
action_value_mappings:
auto-close:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: close的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: close参数
type: object
type: UniLabJsonCommand
auto-is_closed:
feedback: {}
goal: {}
@@ -309,50 +217,6 @@ vacuum_pump.mock:
title: is_open参数
type: object
type: UniLabJsonCommand
auto-open:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: open的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: open参数
type: object
type: UniLabJsonCommand
auto-set_status:
feedback: {}
goal: {}
goal_default:
string: null
handles: []
result: {}
schema:
description: set_status的参数schema
properties:
feedback: {}
goal:
properties:
string:
type: string
required:
- string
type: object
result: {}
required:
- goal
title: set_status参数
type: object
type: UniLabJsonCommand
close:
feedback: {}
goal: {}
@@ -360,22 +224,19 @@ vacuum_pump.mock:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -395,22 +256,19 @@ vacuum_pump.mock:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -432,16 +290,14 @@ vacuum_pump.mock:
handles: []
result: {}
schema:
description: ROS Action StrSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: StrSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
string:
type: string
@@ -450,7 +306,6 @@ vacuum_pump.mock:
title: StrSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -470,7 +325,7 @@ vacuum_pump.mock:
status_types:
status: str
type: python
description: Mock vacuum pump
description: 模拟真空泵设备,用于系统测试和开发调试。该设备模拟真实真空泵的抽气功能和状态控制,支持真空系统的启停操作和状态监测。提供与真实真空泵相同的接口和控制逻辑,便于在没有实际硬件的情况下进行真空系统的集成测试。适用于真空工艺调试、软件开发和实验流程验证等场景。
handles:
- data_key: fluid_in
data_source: handle
@@ -494,3 +349,4 @@ vacuum_pump.mock:
required:
- status
type: object
version: 0.0.1

File diff suppressed because it is too large Load Diff

View File

@@ -8,22 +8,19 @@ zhida_hplc:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -36,26 +33,6 @@ zhida_hplc:
title: EmptyIn
type: object
type: EmptyIn
auto-abort:
feedback: {}
goal: {}
goal_default: {}
handles: []
result: {}
schema:
description: abort的参数schema
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: abort参数
type: object
type: UniLabJsonCommand
auto-close:
feedback: {}
goal: {}
@@ -63,7 +40,7 @@ zhida_hplc:
handles: []
result: {}
schema:
description: close的参数schema
description: HPLC设备连接关闭函数。安全地断开与智达HPLC设备的TCP socket连接释放网络资源。该函数确保连接的正确关闭避免网络资源泄露。通常在设备使用完毕或系统关闭时调用。
properties:
feedback: {}
goal:
@@ -83,7 +60,7 @@ zhida_hplc:
handles: []
result: {}
schema:
description: connect的参数schema
description: HPLC设备连接建立函数。与智达HPLC设备建立TCP socket通信连接配置通信超时参数。该函数是设备使用前的必要步骤建立成功后可进行状态查询、方法获取、任务启动等操作。连接失败时会抛出异常。
properties:
feedback: {}
goal:
@@ -96,30 +73,6 @@ zhida_hplc:
title: connect参数
type: object
type: UniLabJsonCommand
auto-start:
feedback: {}
goal: {}
goal_default:
text: null
handles: []
result: {}
schema:
description: start的参数schema
properties:
feedback: {}
goal:
properties:
text:
type: string
required:
- text
type: object
result: {}
required:
- goal
title: start参数
type: object
type: UniLabJsonCommand
get_methods:
feedback: {}
goal: {}
@@ -127,22 +80,19 @@ zhida_hplc:
handles: []
result: {}
schema:
description: ROS Action EmptyIn 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: EmptyIn_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties: {}
required: []
title: EmptyIn_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -164,16 +114,14 @@ zhida_hplc:
handles: []
result: {}
schema:
description: ROS Action StrSingleInput 的 JSON Schema
description: ''
properties:
feedback:
description: Action 反馈 - 执行过程中从服务器发送到客户端
properties: {}
required: []
title: StrSingleInput_Feedback
type: object
goal:
description: Action 目标 - 从客户端发送到服务器
properties:
string:
type: string
@@ -182,7 +130,6 @@ zhida_hplc:
title: StrSingleInput_Goal
type: object
result:
description: Action 结果 - 完成后从服务器发送到客户端
properties:
return_info:
type: string
@@ -203,7 +150,7 @@ zhida_hplc:
methods: dict
status: dict
type: python
description: Zhida HPLC
description: 智达高效液相色谱HPLC分析设备用于实验室样品的分离、检测和定量分析。该设备通过TCP socket与HPLC控制系统通信支持远程控制和状态监控。具备自动进样、梯度洗脱、多检测器数据采集等功能可执行复杂的色谱分析方法。适用于化学分析、药物检测、环境监测、生物样品分析等需要高精度分离分析的实验室应用场景。
handles: []
icon: ''
init_param_schema:
@@ -230,3 +177,4 @@ zhida_hplc:
- status
- methods
type: object
version: 0.0.1

View File

@@ -2,6 +2,8 @@ import copy
import io
import os
import sys
import inspect
import importlib
from pathlib import Path
from typing import Any, Dict, List
@@ -63,7 +65,7 @@ class Registry:
},
"feedback": {},
"result": {"success": "success"},
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuter),
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuter, '用于创建或更新物料资源,每次传入多个物料信息。'),
"goal_default": yaml.safe_load(
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuter.Goal))
),
@@ -84,7 +86,7 @@ class Registry:
},
"feedback": {},
"result": {"success": "success"},
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy),
"schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy, '用于创建或更新物料资源,每次传入一个物料信息。'),
"goal_default": yaml.safe_load(
io.StringIO(get_yaml_from_goal_type(self.ResourceCreateFromOuterEasy.Goal))
),
@@ -99,18 +101,25 @@ class Registry:
}
]
},
# todo: support nested keys, switch to non ros message schema
"placeholder_keys": {
"res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择
"device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
"parent": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择
},
},
"test_latency": {
"type": self.EmptyIn,
"goal": {},
"feedback": {},
"result": {"latency_ms": "latency_ms", "time_diff_ms": "time_diff_ms"},
"schema": ros_action_to_json_schema(self.EmptyIn),
"schema": ros_action_to_json_schema(self.EmptyIn, '用于测试延迟的动作,返回延迟时间和时间差。'),
"goal_default": {},
"handles": {},
},
},
},
"version": "0.0.1",
"icon": "icon_device.webp",
"registry_type": "device",
"handles": [],
@@ -161,6 +170,54 @@ class Registry:
else:
logger.debug(f"[UniLab Registry] Res File-{i+1}/{len(files)} Not Valid YAML File: {file.absolute()}")
def _extract_class_docstrings(self, module_string: str) -> Dict[str, str]:
"""
从模块字符串中提取类和方法的docstring信息
Args:
module_string: 模块字符串,格式为 "module.path:ClassName"
Returns:
包含类和方法docstring信息的字典
"""
docstrings = {"class_docstring": "", "methods": {}}
if not module_string or ":" not in module_string:
return docstrings
try:
module_path, class_name = module_string.split(":", 1)
# 动态导入模块
module = importlib.import_module(module_path)
# 获取类
if hasattr(module, class_name):
cls = getattr(module, class_name)
# 获取类的docstring
class_doc = inspect.getdoc(cls)
if class_doc:
docstrings["class_docstring"] = class_doc.strip()
# 获取所有方法的docstring
for method_name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
method_doc = inspect.getdoc(method)
if method_doc:
docstrings["methods"][method_name] = method_doc.strip()
# 也获取属性方法的docstring
for method_name, method in inspect.getmembers(cls, predicate=lambda x: isinstance(x, property)):
if hasattr(method, "fget") and method.fget:
method_doc = inspect.getdoc(method.fget)
if method_doc:
docstrings["methods"][method_name] = method_doc.strip()
except Exception as e:
logger.warning(f"[UniLab Registry] 无法提取docstring信息模块: {module_string}, 错误: {str(e)}")
return docstrings
def _replace_type_with_class(self, type_name: str, device_id: str, field_name: str) -> Any:
"""
将类型名称替换为实际的类对象
@@ -274,15 +331,13 @@ class Registry:
param_type = arg_info.get("type", "")
param_default = arg_info.get("default")
param_required = arg_info.get("required", True)
schema["properties"][param_name] = self._generate_schema_from_info(
param_name, param_type, param_default
)
schema["properties"][param_name] = self._generate_schema_from_info(param_name, param_type, param_default)
if param_required:
schema["required"].append(param_name)
return {
"title": f"{method_name}参数",
"description": f"{method_name}的参数schema",
"description": f"",
"type": "object",
"properties": {"goal": schema, "feedback": {}, "result": {}},
"required": ["goal"],
@@ -313,6 +368,8 @@ class Registry:
# 在添加到注册表前处理类型替换
for device_id, device_config in data.items():
# 添加文件路径信息 - 使用规范化的完整文件路径
if "version" not in device_config:
device_config["version"] = "0.0.1"
if "description" not in device_config:
device_config["description"] = ""
if "icon" not in device_config:
@@ -348,6 +405,14 @@ class Registry:
sorted(device_config["class"]["status_types"].items())
)
if complete_registry:
# 保存原有的description信息
old_descriptions = {}
for action_name, action_config in device_config["class"]["action_value_mappings"].items():
if "description" in action_config.get("schema", {}):
description = action_config["schema"]["description"]
if len(description):
old_descriptions[action_name] = action_config["schema"]["description"]
device_config["class"]["action_value_mappings"] = {
k: v
for k, v in device_config["class"]["action_value_mappings"].items()
@@ -365,9 +430,15 @@ class Registry:
"goal_default": {i["name"]: i["default"] for i in v["args"]},
"handles": [],
}
# 不生成已配置action的动作
for k, v in enhanced_info["action_methods"].items()
if k not in device_config["class"]["action_value_mappings"]
}
)
# 恢复原有的description信息auto开头的不修改
for action_name, description in old_descriptions.items():
device_config["class"]["action_value_mappings"][action_name]["schema"]["description"] = description
device_config["init_param_schema"] = {}
device_config["init_param_schema"]["config"] = self._generate_unilab_json_command_schema(
enhanced_info["init_params"], "__init__"
@@ -471,6 +542,13 @@ class Registry:
},
**schema["properties"]["goal"]["properties"],
}
# 将 placeholder_keys 信息添加到 schema 中
if "placeholder_keys" in action_config and action_config.get("schema", {}).get(
"properties", {}
).get("goal", {}):
action_config["schema"]["properties"]["goal"]["_unilabos_placeholder_info"] = action_config[
"placeholder_keys"
]
msg = {"id": device_id, **device_info_copy}
devices.append(msg)

View File

@@ -132,7 +132,11 @@ _msg_converter: Dict[Type, Any] = {
Bool: lambda x: Bool(data=bool(x)),
str: str,
String: lambda x: String(data=str(x)),
Point: lambda x: Point(x=x.x, y=x.y, z=x.z) if not isinstance(x, dict) else Point(x=x.get("x", 0.0), y=x.get("y", 0.0), z=x.get("z", 0.0)),
Point: lambda x: (
Point(x=x.x, y=x.y, z=x.z)
if not isinstance(x, dict)
else Point(x=float(x.get("x", 0.0)), y=float(x.get("y", 0.0)), z=float(x.get("z", 0.0)))
),
Resource: lambda x: Resource(
id=x.get("id", ""),
name=x.get("name", ""),
@@ -142,7 +146,13 @@ _msg_converter: Dict[Type, Any] = {
type=x.get("type", ""),
category=x.get("class", "") or x.get("type", ""),
pose=(
Pose(position=Point(x=float(x.get("position", {}).get("x", 0.0)), y=float(x.get("position", {}).get("y", 0.0)), z=float(x.get("position", {}).get("z", 0.0))))
Pose(
position=Point(
x=float(x.get("position", {}).get("x", 0.0)),
y=float(x.get("position", {}).get("y", 0.0)),
z=float(x.get("position", {}).get("z", 0.0)),
)
)
if x.get("position", None) is not None
else Pose()
),
@@ -151,6 +161,7 @@ _msg_converter: Dict[Type, Any] = {
),
}
def json_or_yaml_loads(data: str) -> Any:
try:
return json.loads(data)
@@ -161,6 +172,7 @@ def json_or_yaml_loads(data: str) -> Any:
pass
raise e
# ROS消息到Python转换器
_msg_converter_back: Dict[Type, Any] = {
float: float,
@@ -571,27 +583,27 @@ from unilabos.utils.import_manager import ImportManager
from unilabos.config.config import ROSConfig
basic_type_map = {
'bool': {'type': 'boolean'},
'int8': {'type': 'integer', 'minimum': -128, 'maximum': 127},
'uint8': {'type': 'integer', 'minimum': 0, 'maximum': 255},
'int16': {'type': 'integer', 'minimum': -32768, 'maximum': 32767},
'uint16': {'type': 'integer', 'minimum': 0, 'maximum': 65535},
'int32': {'type': 'integer', 'minimum': -2147483648, 'maximum': 2147483647},
'uint32': {'type': 'integer', 'minimum': 0, 'maximum': 4294967295},
'int64': {'type': 'integer'},
'uint64': {'type': 'integer', 'minimum': 0},
'double': {'type': 'number'},
'float': {'type': 'number'},
'float32': {'type': 'number'},
'float64': {'type': 'number'},
'string': {'type': 'string'},
'boolean': {'type': 'boolean'},
'char': {'type': 'string', 'maxLength': 1},
'byte': {'type': 'integer', 'minimum': 0, 'maximum': 255},
"bool": {"type": "boolean"},
"int8": {"type": "integer", "minimum": -128, "maximum": 127},
"uint8": {"type": "integer", "minimum": 0, "maximum": 255},
"int16": {"type": "integer", "minimum": -32768, "maximum": 32767},
"uint16": {"type": "integer", "minimum": 0, "maximum": 65535},
"int32": {"type": "integer", "minimum": -2147483648, "maximum": 2147483647},
"uint32": {"type": "integer", "minimum": 0, "maximum": 4294967295},
"int64": {"type": "integer"},
"uint64": {"type": "integer", "minimum": 0},
"double": {"type": "number"},
"float": {"type": "number"},
"float32": {"type": "number"},
"float64": {"type": "number"},
"string": {"type": "string"},
"boolean": {"type": "boolean"},
"char": {"type": "string", "maxLength": 1},
"byte": {"type": "integer", "minimum": 0, "maximum": 255},
}
def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str=None) -> Dict[str, Any]:
def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str = None) -> Dict[str, Any]:
"""
将 ROS 字段类型转换为 JSON Schema 类型定义
@@ -603,10 +615,7 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str=None) ->
对应的 JSON Schema 类型定义
"""
if isinstance(type_info, UnboundedSequence):
return {
'type': 'array',
'items': ros_field_type_to_json_schema(type_info.value_type)
}
return {"type": "array", "items": ros_field_type_to_json_schema(type_info.value_type)}
if isinstance(type_info, NamespacedType):
cls_name = ".".join(type_info.namespaces) + ":" + type_info.name
type_class = msg_converter_manager.get_class(cls_name)
@@ -614,20 +623,20 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str=None) ->
elif isinstance(type_info, BasicType):
return ros_field_type_to_json_schema(type_info.typename)
elif isinstance(type_info, UnboundedString):
return basic_type_map['string']
return basic_type_map["string"]
elif isinstance(type_info, str):
if type_info in basic_type_map:
return basic_type_map[type_info]
# 处理时间和持续时间类型
if type_info in ('time', 'duration', 'builtin_interfaces/Time', 'builtin_interfaces/Duration'):
if type_info in ("time", "duration", "builtin_interfaces/Time", "builtin_interfaces/Duration"):
return {
'type': 'object',
'properties': {
'sec': {'type': 'integer', 'description': ''},
'nanosec': {'type': 'integer', 'description': '纳秒'}
"type": "object",
"properties": {
"sec": {"type": "integer", "description": ""},
"nanosec": {"type": "integer", "description": "纳秒"},
},
'required': ['sec', 'nanosec']
"required": ["sec", "nanosec"],
}
else:
return ros_message_to_json_schema(type_info)
@@ -639,8 +648,6 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str=None) ->
# 'items': ros_field_type_to_json_schema(item_type)
# }
# # 处理复杂类型(尝试加载并处理)
# try:
# # 如果它是一个完整的消息类型规范 (包名/msg/类型名)
@@ -655,6 +662,7 @@ def ros_field_type_to_json_schema(type_info: Type | str, slot_type: str=None) ->
# logger.debug(f"无法解析类型 {field_type}: {str(e)}")
# return {'type': 'object', 'description': f'未知类型: {field_type}'}
def ros_message_to_json_schema(msg_class: Any) -> Dict[str, Any]:
"""
将 ROS 消息类转换为 JSON Schema
@@ -665,15 +673,11 @@ def ros_message_to_json_schema(msg_class: Any) -> Dict[str, Any]:
Returns:
对应的 JSON Schema 定义
"""
schema = {
'type': 'object',
'properties': {},
'required': []
}
schema = {"type": "object", "properties": {}, "required": []}
# 获取类名作为标题
if hasattr(msg_class, '__name__'):
schema['title'] = msg_class.__name__
if hasattr(msg_class, "__name__"):
schema["title"] = msg_class.__name__
# 获取消息的字段和字段类型
try:
@@ -681,8 +685,8 @@ def ros_message_to_json_schema(msg_class: Any) -> Dict[str, Any]:
slot_name, slot_type = slot_info
type_info = msg_class.SLOT_TYPES[ind]
field_schema = ros_field_type_to_json_schema(type_info, slot_type)
schema['properties'][slot_name] = field_schema
schema['required'].append(slot_name)
schema["properties"][slot_name] = field_schema
schema["required"].append(slot_name)
# if hasattr(msg_class, 'get_fields_and_field_types'):
# fields_and_types = msg_class.get_fields_and_field_types()
#
@@ -707,52 +711,57 @@ def ros_message_to_json_schema(msg_class: Any) -> Dict[str, Any]:
# schema['required'].append(clean_name)
except Exception as e:
# 如果获取字段类型失败,添加错误信息
schema['description'] = f"解析消息字段时出错: {str(e)}"
schema["description"] = f"解析消息字段时出错: {str(e)}"
logger.error(f"解析 {msg_class.__name__} 消息字段失败: {str(e)}")
return schema
def ros_action_to_json_schema(action_class: Any) -> Dict[str, Any]:
def ros_action_to_json_schema(action_class: Any, description="") -> Dict[str, Any]:
"""
将 ROS Action 类转换为 JSON Schema
Args:
action_class: ROS Action 类
description: 描述
Returns:
完整的 JSON Schema 定义
"""
if not hasattr(action_class, 'Goal') or not hasattr(action_class, 'Feedback') or not hasattr(action_class, 'Result'):
if (
not hasattr(action_class, "Goal")
or not hasattr(action_class, "Feedback")
or not hasattr(action_class, "Result")
):
raise ValueError(f"{action_class.__name__} 不是有效的 ROS Action 类")
# 创建基础 schema
schema = {
'title': action_class.__name__,
'description': f"ROS Action {action_class.__name__} 的 JSON Schema",
'type': 'object',
'properties': {
'goal': {
'description': 'Action 目标 - 从客户端发送到服务器',
"title": action_class.__name__,
"description": description,
"type": "object",
"properties": {
"goal": {
# 'description': 'Action 目标 - 从客户端发送到服务器',
**ros_message_to_json_schema(action_class.Goal)
},
'feedback': {
'description': 'Action 反馈 - 执行过程中从服务器发送到客户端',
"feedback": {
# 'description': 'Action 反馈 - 执行过程中从服务器发送到客户端',
**ros_message_to_json_schema(action_class.Feedback)
},
'result': {
'description': 'Action 结果 - 完成后从服务器发送到客户端',
"result": {
# 'description': 'Action 结果 - 完成后从服务器发送到客户端',
**ros_message_to_json_schema(action_class.Result)
}
},
},
'required': ['goal']
"required": ["goal"],
}
return schema
def convert_ros_action_to_jsonschema(
action_name_or_type: Union[str, Type],
output_file: Optional[str] = None,
format: str = 'json'
action_name_or_type: Union[str, Type], output_file: Optional[str] = None, format: str = "json"
) -> Dict[str, Any]:
"""
将 ROS Action 类型转换为 JSON Schema并可选地保存到文件
@@ -777,11 +786,11 @@ def convert_ros_action_to_jsonschema(
# 如果指定了输出文件,将 Schema 保存到文件
if output_file:
if format.lower() == 'json':
with open(output_file, 'w', encoding='utf-8') as f:
if format.lower() == "json":
with open(output_file, "w", encoding="utf-8") as f:
json.dump(schema, f, indent=2, ensure_ascii=False)
elif format.lower() == 'yaml':
with open(output_file, 'w', encoding='utf-8') as f:
elif format.lower() == "yaml":
with open(output_file, "w", encoding="utf-8") as f:
yaml.safe_dump(schema, f, default_flow_style=False, allow_unicode=True)
else:
raise ValueError(f"不支持的格式: {format},请使用 'json''yaml'")

View File

@@ -307,7 +307,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 创建动作服务
if self.create_action_server:
for action_name, action_value_mapping in self._action_value_mappings.items():
if action_name.startswith("auto-"):
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith("UniLabJsonCommand"):
continue
self.create_ros_action_server(action_name, action_value_mapping)
@@ -923,11 +923,18 @@ class ROS2DeviceNode:
driver_class.__module__.startswith("pylabrobot")
or driver_class.__name__ == "LiquidHandlerAbstract"
or driver_class.__name__ == "LiquidHandlerBiomek"
or driver_class.__name__ == "PRCXI9300Handler"
)
# TODO: 要在创建之前预先请求服务器是否有当前id的物料放到resource_tracker中让pylabrobot进行创建
# 创建设备类实例
if use_pylabrobot_creator:
# 先对pylabrobot的子资源进行加载不然subclass无法认出
# 在下方对于加载Deck等Resource要手动import
# noinspection PyUnresolvedReferences
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Deck
# noinspection PyUnresolvedReferences
from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container
self._driver_creator = PyLabRobotCreator(
driver_class, children=children, resource_tracker=self.resource_tracker
)

View File

@@ -459,7 +459,7 @@ class HostNode(BaseROS2DeviceNode):
self.devices_instances[device_id] = d
# noinspection PyProtectedMember
for action_name, action_value_mapping in d._ros_node._action_value_mappings.items():
if action_name.startswith("auto-"):
if action_name.startswith("auto-") or str(action_value_mapping.get("type", "")).startswith("UniLabJsonCommand"):
continue
action_id = f"/devices/{device_id}/{action_name}"
if action_id not in self._action_clients:
@@ -603,8 +603,7 @@ class HostNode(BaseROS2DeviceNode):
if action_name == "test_latency" and server_info is not None:
self.server_latest_timestamp = server_info.get("send_timestamp", 0.0)
if action_id not in self._action_clients:
self.lab_logger().error(f"[Host Node] ActionClient {action_id} not found.")
return
raise ValueError(f"ActionClient {action_id} not found.")
action_client: ActionClient = self._action_clients[action_id]

View File

@@ -134,7 +134,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
if d is not None and hasattr(d, "ros_node_instance"):
node = d.ros_node_instance
for action_name, action_mapping in node._action_value_mappings.items():
if action_name.startswith("auto-"):
if action_name.startswith("auto-") or str(action_mapping.get("type", "")).startswith("UniLabJsonCommand"):
continue
action_id = f"/devices/{device_id_abs}/{action_name}"
if action_id not in self._action_clients:
@@ -211,7 +211,7 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
# 逐步执行工作流
step_results = []
for i, action in enumerate(protocol_steps):
self.get_logger().info(f"Running step {i + 1}: {action}")
# self.get_logger().info(f"Running step {i + 1}: {action}")
if isinstance(action, dict):
# 如果是单个动作,直接执行
if action["action_name"] == "wait":

View File

@@ -148,7 +148,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
contain_model = not issubclass(target_type, Deck)
resource, target_type = self._process_resource_mapping(resource, target_type)
resource_instance: Resource = resource_ulab_to_plr(resource, contain_model)
states[prefix_path] = resource_instance.serialize_all_state()
# 使用 prefix_path 作为 key 存储资源状态
if to_dict:
serialized = resource_instance.serialize()
@@ -199,7 +199,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
spect = inspect.signature(deserialize_method)
spec_args = spect.parameters
for param_name, param_value in data.copy().items():
if "_resource_child_name" in param_value and "_resource_type" not in param_value:
if isinstance(param_value, dict) and "_resource_child_name" in param_value and "_resource_type" not in param_value:
arg_value = spec_args[param_name].annotation
data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value
logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}")
@@ -230,7 +230,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
spect = inspect.signature(self.device_cls.__init__)
spec_args = spect.parameters
for param_name, param_value in data.copy().items():
if "_resource_child_name" in param_value and "_resource_type" not in param_value:
if isinstance(param_value, dict) and "_resource_child_name" in param_value and "_resource_type" not in param_value:
arg_value = spec_args[param_name].annotation
data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value
logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}")

View File

@@ -148,7 +148,7 @@ def configure_logger():
"""配置日志记录器"""
# 获取根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG) # 修改为DEBUG以显示所有级别
root_logger.setLevel(logging.INFO) # 修改为DEBUG以显示所有级别
# 移除已存在的处理器
for handler in root_logger.handlers[:]:
@@ -156,7 +156,7 @@ def configure_logger():
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.DEBUG) # 修改为DEBUG以显示所有级别
console_handler.setLevel(logging.INFO) # 修改为DEBUG以显示所有级别
# 使用自定义的颜色格式化器
color_formatter = ColoredFormatter()