bump version & protocol fix

This commit is contained in:
Xuwznln
2025-06-12 21:21:25 +08:00
parent 96f37b3b0d
commit 11e4f053f1
10 changed files with 197 additions and 253 deletions

View File

@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
# Currently, you need to install the `unilabos_msgs` package # Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page # You can download the system-specific package from the Release page
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.5-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites # Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包 # 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装 # 可以前往 Release 页面下载系统对应的包进行安装
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.5-xxxxx.tar.bz2
# 安装PyLabRobot等前置 # 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

@@ -1,6 +1,6 @@
package: package:
name: ros-humble-unilabos-msgs name: ros-humble-unilabos-msgs
version: 0.9.4 version: 0.9.5
source: source:
path: ../../unilabos_msgs path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work folder: ros-humble-unilabos-msgs/src/work

View File

@@ -1,6 +1,6 @@
package: package:
name: unilabos name: unilabos
version: "0.9.4" version: "0.9.5"
source: source:
path: ../.. path: ../..

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup( setup(
name=package_name, name=package_name,
version='0.9.4', version='0.9.5',
packages=find_packages(), packages=find_packages(),
include_package_data=True, include_package_data=True,
install_requires=['setuptools'], install_requires=['setuptools'],

View File

@@ -2,6 +2,31 @@ import numpy as np
import networkx as nx import networkx as nx
def is_integrated_pump(node_name):
return "pump" in node_name and "valve" in node_name
def find_connected_pump(G, valve_node):
for neighbor in G.neighbors(valve_node):
if "pump" in G.nodes[neighbor]["class"]:
return neighbor
raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点")
def build_pump_valve_maps(G, pump_backbone):
pumps_from_node = {}
valve_from_node = {}
for node in pump_backbone:
if is_integrated_pump(node):
pumps_from_node[node] = node
valve_from_node[node] = node
else:
pump_node = find_connected_pump(G, node)
pumps_from_node[node] = pump_node
valve_from_node[node] = node
return pumps_from_node, valve_from_node
def generate_pump_protocol( def generate_pump_protocol(
G: nx.DiGraph, G: nx.DiGraph,
from_vessel: str, from_vessel: str,
@@ -24,27 +49,10 @@ def generate_pump_protocol(
# 生成泵操作的动作序列 # 生成泵操作的动作序列
pump_action_sequence = [] pump_action_sequence = []
nodes = G.nodes(data=True)
# 检查节点是否存在 # 从from_vessel到to_vessel的最短路径
if from_vessel not in G.nodes:
print(f"Warning: Source vessel '{from_vessel}' not found in graph. Skipping.")
return []
if to_vessel not in G.nodes:
print(f"Warning: Target vessel '{to_vessel}' not found in graph. Skipping.")
return []
# 检查是否存在路径
try:
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
except nx.NetworkXNoPath: print(shortest_path)
print(f"Warning: No path from '{from_vessel}' to '{to_vessel}'. Skipping.")
return []
except nx.NodeNotFound as e:
print(f"Warning: Node not found: {e}. Skipping.")
return []
print(f"Shortest path: {shortest_path}")
pump_backbone = shortest_path pump_backbone = shortest_path
if not from_vessel.startswith("pump"): if not from_vessel.startswith("pump"):
@@ -52,34 +60,12 @@ def generate_pump_protocol(
if not to_vessel.startswith("pump"): if not to_vessel.startswith("pump"):
pump_backbone = pump_backbone[:-1] pump_backbone = pump_backbone[:-1]
print(f"Pump backbone: {pump_backbone}")
# 修复检查pump_backbone是否为空
if not pump_backbone:
print(f"Warning: No pumps found in path from '{from_vessel}' to '{to_vessel}'. Skipping.")
return []
if transfer_flowrate == 0: if transfer_flowrate == 0:
transfer_flowrate = flowrate transfer_flowrate = flowrate
# 修复:正确访问节点数据 pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
pump_max_volumes = []
for pump in pump_backbone:
# 直接使用 G.nodes[pump] 来访问节点数据
pump_data = G.nodes[pump] if pump in G.nodes else {}
# 尝试多种可能的键名,并提供默认值
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
if max_vol is None:
# 如果是设备节点尝试从config中获取
config = pump_data.get('config', {})
max_vol = config.get('max_volume', 25.0)
pump_max_volumes.append(float(max_vol))
if pump_max_volumes:
min_transfer_volume = min(pump_max_volumes)
else:
min_transfer_volume = 25.0 # 默认值
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
repeats = int(np.ceil(volume / min_transfer_volume)) repeats = int(np.ceil(volume / min_transfer_volume))
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.") raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
@@ -89,20 +75,17 @@ def generate_pump_protocol(
# 生成泵操作的动作序列 # 生成泵操作的动作序列
for i in range(repeats): for i in range(repeats):
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵 # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
if not from_vessel.startswith("pump") and pump_backbone: if not from_vessel.startswith("pump"):
# 修复:添加边缘数据检查
edge_data = G.get_edge_data(pump_backbone[0], from_vessel)
if edge_data and "port" in edge_data:
pump_action_sequence.extend([ pump_action_sequence.extend([
{ {
"device_id": pump_backbone[0], "device_id": valve_from_node[pump_backbone[0]],
"action_name": "set_valve_position", "action_name": "set_valve_position",
"action_kwargs": { "action_kwargs": {
"command": edge_data["port"][pump_backbone[0]] "command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
} }
}, },
{ {
"device_id": pump_backbone[0], "device_id": pumps_from_node[pump_backbone[0]],
"action_name": "set_position", "action_name": "set_position",
"action_kwargs": { "action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)), "position": float(min(volume_left, min_transfer_volume)),
@@ -111,37 +94,28 @@ def generate_pump_protocol(
} }
]) ])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
else: for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
print(f"Warning: No edge data found between {pump_backbone[0]} and {from_vessel}")
# 修复检查pump_backbone长度避免多泵操作时出错
if len(pump_backbone) > 1:
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]):
# 相邻两泵同时切换阀门至连通位置 # 相邻两泵同时切换阀门至连通位置
edge_AB = G.get_edge_data(pumpA, pumpB)
edge_BA = G.get_edge_data(pumpB, pumpA)
if edge_AB and "port" in edge_AB and edge_BA and "port" in edge_BA:
pump_action_sequence.append([ pump_action_sequence.append([
{ {
"device_id": pumpA, "device_id": valve_from_node[nodeA],
"action_name": "set_valve_position", "action_name": "set_valve_position",
"action_kwargs": { "action_kwargs": {
"command": edge_AB["port"][pumpA] "command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
} }
}, },
{ {
"device_id": pumpB, "device_id": valve_from_node[nodeB],
"action_name": "set_valve_position", "action_name": "set_valve_position",
"action_kwargs": { "action_kwargs": {
"command": edge_BA["port"][pumpB], "command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
} }
} }
]) ])
# 相邻两泵液体转移泵A排出液体泵B吸入液体 # 相邻两泵液体转移泵A排出液体泵B吸入液体
pump_action_sequence.append([ pump_action_sequence.append([
{ {
"device_id": pumpA, "device_id": pumps_from_node[nodeA],
"action_name": "set_position", "action_name": "set_position",
"action_kwargs": { "action_kwargs": {
"position": 0.0, "position": 0.0,
@@ -149,7 +123,7 @@ def generate_pump_protocol(
} }
}, },
{ {
"device_id": pumpB, "device_id": pumps_from_node[nodeB],
"action_name": "set_position", "action_name": "set_position",
"action_kwargs": { "action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)), "position": float(min(volume_left, min_transfer_volume)),
@@ -158,23 +132,19 @@ def generate_pump_protocol(
} }
]) ])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
else:
print(f"Warning: No edge data found between {pumpA} and {pumpB}")
if not to_vessel.startswith("pump") and pump_backbone: if not to_vessel.startswith("pump"):
# 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B # 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B
edge_data = G.get_edge_data(pump_backbone[-1], to_vessel)
if edge_data and "port" in edge_data:
pump_action_sequence.extend([ pump_action_sequence.extend([
{ {
"device_id": pump_backbone[-1], "device_id": valve_from_node[pump_backbone[-1]],
"action_name": "set_valve_position", "action_name": "set_valve_position",
"action_kwargs": { "action_kwargs": {
"command": edge_data["port"][pump_backbone[-1]] "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
} }
}, },
{ {
"device_id": pump_backbone[-1], "device_id": pumps_from_node[pump_backbone[-1]],
"action_name": "set_position", "action_name": "set_position",
"action_kwargs": { "action_kwargs": {
"position": 0.0, "position": 0.0,
@@ -183,8 +153,6 @@ def generate_pump_protocol(
} }
]) ])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
else:
print(f"Warning: No edge data found between {pump_backbone[-1]} and {to_vessel}")
volume_left -= min_transfer_volume volume_left -= min_transfer_volume
return pump_action_sequence return pump_action_sequence
@@ -233,86 +201,54 @@ def generate_pump_protocol_with_rinsing(
Examples: Examples:
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water") pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
""" """
# 修复:使用实际存在的节点名称 air_vessel = "flask_air"
air_vessel = "flask_air" # 这个在你的配置中存在 waste_vessel = f"waste_workup"
# 寻找合适的废料容器,如果没有找到则使用空的容器作为替代
waste_vessel = None
available_vessels = [node for node in G.nodes if node.startswith("flask_") and node != air_vessel]
if available_vessels:
# 使用第一个可用的容器作为废料容器
waste_vessel = available_vessels[0]
print(f"Using {waste_vessel} as waste vessel")
else:
waste_vessel = "flask_1" # 备用选择
# 修复:添加路径检查
try:
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
pump_backbone = shortest_path[1: -1] pump_backbone = shortest_path[1: -1]
except (nx.NetworkXNoPath, nx.NodeNotFound) as e: nodes = G.nodes(data=True)
print(f"Warning: Cannot find path from {from_vessel} to {to_vessel}: {e}")
return []
# 修复:正确访问节点数据 pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
pump_max_volumes = []
for pump in pump_backbone:
# 直接使用 G.nodes[pump] 来访问节点数据
pump_data = G.nodes[pump] if pump in G.nodes else {}
# 尝试多种可能的键名,并提供默认值
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
if max_vol is None:
# 如果是设备节点尝试从config中获取
config = pump_data.get('config', {})
max_vol = config.get('max_volume', 25.0)
pump_max_volumes.append(float(max_vol))
if pump_max_volumes:
min_transfer_volume = float(min(pump_max_volumes))
else:
min_transfer_volume = 25.0 # 默认值
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
if time != 0: if time != 0:
flowrate = transfer_flowrate = volume / time flowrate = transfer_flowrate = volume / time
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
if rinsing_solvent != "air" and rinsing_solvent != "":
# 修复:只在需要清洗且相关节点存在时才执行清洗步骤
if rinsing_solvent != "air" and pump_backbone:
if "," in rinsing_solvent: if "," in rinsing_solvent:
rinsing_solvents = rinsing_solvent.split(",") rinsing_solvents = rinsing_solvent.split(",")
assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats." assert len(
rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
else: else:
rinsing_solvents = [rinsing_solvent] * rinsing_repeats rinsing_solvents = [rinsing_solvent] * rinsing_repeats
for rinsing_solvent in rinsing_solvents: for rinsing_solvent in rinsing_solvents:
solvent_vessel = f"flask_{rinsing_solvent}" solvent_vessel = f"flask_{rinsing_solvent}"
# 清洗泵
# 检查溶剂容器是否存在
if solvent_vessel not in G.nodes:
print(f"Warning: Solvent vessel '{solvent_vessel}' not found in graph. Skipping rinsing step.")
continue
# 清洗泵 - 只有当所有必需的节点都存在且pump_backbone不为空时才执行
if pump_backbone and len(pump_backbone) > 0 and waste_vessel in G.nodes:
pump_action_sequence.extend( pump_action_sequence.extend(
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate,
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate) generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate,
transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate,
transfer_flowrate)
) )
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。 # 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
if rinsing_solvent == rinsing_solvents[0]: if rinsing_solvent == rinsing_solvents[0]:
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate)) pump_action_sequence.extend(
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate)) generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) pump_action_sequence.extend(
generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
# 最后的空气清洗 - 只有当节点存在时才执行 pump_action_sequence.extend(
if air_vessel in G.nodes: generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) if rinsing_solvent != "":
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) pump_action_sequence.extend(
generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
pump_action_sequence.extend(
generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
return pump_action_sequence return pump_action_sequence
# End Protocols # End Protocols

View File

@@ -1,7 +1,7 @@
io_snrd: io_snrd:
description: IO Board with 16 IOs description: IO Board with 16 IOs
class: class:
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO module: ilabos.device_comms.SRND_16_IO:SRND_16_IO
type: python type: python
hardware_interface: hardware_interface:
name: modbus_client name: modbus_client

View File

@@ -71,3 +71,13 @@ solenoid_valve:
class: class:
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
type: python type: python
status_types:
status: String
valve_position: String
action_value_mappings:
set_valve_position:
type: StrSingleInput
goal:
string: position
feedback: {}
result: {}

View File

@@ -601,10 +601,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
goal = goal_handle.request goal = goal_handle.request
# 从目标消息中提取参数, 并调用对应的方法 # 从目标消息中提取参数, 并调用对应的方法
if "sequence" in self._action_value_mappings: if "sequence" in action_value_mapping:
# 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用 # 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用
def ACTION(**kwargs): def ACTION(**kwargs):
for i, action in enumerate(self._action_value_mappings["sequence"]): for i, action in enumerate(action_value_mapping["sequence"]):
if i == 0: if i == 0:
self.lab_logger().info(f"执行序列动作第一步: {action}") self.lab_logger().info(f"执行序列动作第一步: {action}")
self.get_real_function(self.driver_instance, action)[0](**kwargs) self.get_real_function(self.driver_instance, action)[0](**kwargs)
@@ -612,9 +612,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self.lab_logger().info(f"执行序列动作后续步骤: {action}") self.lab_logger().info(f"执行序列动作后续步骤: {action}")
self.get_real_function(self.driver_instance, action)[0]() self.get_real_function(self.driver_instance, action)[0]()
action_paramtypes = get_type_hints( action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[1]
self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0])
)[1]
else: else:
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name) ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)

View File

@@ -256,12 +256,12 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
return write_func(*args, **kwargs) return write_func(*args, **kwargs)
if read_method: if read_method:
bound_read = MethodType(_read, device.driver_instance) # bound_read = MethodType(_read, device.driver_instance)
setattr(device.driver_instance, read_method, bound_read) setattr(device.driver_instance, read_method, _read)
if write_method: if write_method:
bound_write = MethodType(_write, device.driver_instance) # bound_write = MethodType(_write, device.driver_instance)
setattr(device.driver_instance, write_method, bound_write) setattr(device.driver_instance, write_method, _write)
async def _update_resources(self, goal, protocol_kwargs): async def _update_resources(self, goal, protocol_kwargs):