bump version to 0.9.7 新增一个测试PumpTransferProtocol的teststation,亲测可以运行,将八通阀们和转移泵与pump_protocol适配

This commit is contained in:
KCFeng425
2025-06-17 16:56:49 +08:00
parent 18c4eb3e4d
commit f6f9244ff1
11 changed files with 464 additions and 76 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.6-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.7-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.6-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.7-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.6 version: 0.9.7
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.6" version: "0.9.7"
source: source:
path: ../.. path: ../..

View File

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

View File

@@ -0,0 +1,304 @@
{
"nodes": [
{
"id": "SimpleProtocolStation",
"name": "简单协议工作站",
"children": [
"transfer_pump_1",
"multiway_valve_1",
"flask_DMF",
"flask_ethyl_acetate",
"flask_methanol",
"main_reactor",
"waste_workup",
"collection_bottle_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": ["PumpTransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵1",
"children": [],
"parent": "SimpleProtocolStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 500,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle",
"valve_position": "0"
}
},
{
"id": "multiway_valve_1",
"name": "八通阀1",
"children": [],
"parent": "SimpleProtocolStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 500,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "flask_DMF",
"name": "DMF试剂瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "DMF",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯试剂瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇试剂瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "methanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液处理瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump_valve",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_valve_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve_DMF",
"source": "multiway_valve_1",
"target": "flask_DMF",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_DMF": "outlet"
}
},
{
"id": "link_valve_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve_reactor",
"source": "multiway_valve_1",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"main_reactor": "inlet"
}
},
{
"id": "link_valve_waste",
"source": "multiway_valve_1",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"waste_workup": "inlet"
}
},
{
"id": "link_valve_collection",
"source": "multiway_valve_1",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_1": "7",
"collection_bottle_1": "inlet"
}
}
]
}

View File

@@ -192,6 +192,16 @@ class VirtualMultiwayValve:
def __str__(self): def __str__(self):
return f"VirtualMultiwayValve(Position: {self._current_position}/{self.max_positions}, Port: {self.get_current_port()}, Status: {self._status})" return f"VirtualMultiwayValve(Position: {self._current_position}/{self.max_positions}, Port: {self.get_current_port()}, Status: {self._status})"
def set_valve_position(self, command: Union[int, str]):
"""
设置阀门位置 - 兼容pump_protocol调用
这是set_position的别名方法用于兼容pump_protocol.py
Args:
command: 目标位置 (0-8) 或位置字符串
"""
return self.set_position(command)
# 使用示例 # 使用示例
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -11,23 +11,39 @@ class VirtualPumpMode(Enum):
AccuratePosVel = 2 AccuratePosVel = 2
class VirtualPump: class VirtualTransferPump:
"""虚拟泵类 - 模拟泵的基本功能,无需实际硬件""" """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件"""
def __init__(self, device_id: str = None, max_volume: float = 25.0, mode: VirtualPumpMode = VirtualPumpMode.Normal, transfer_rate=0): def __init__(self, device_id: str = None, config: dict = None, **kwargs):
self.device_id = device_id or "virtual_pump" """
self.max_volume = max_volume 初始化虚拟转移泵
self._transfer_rate = transfer_rate
self.mode = mode
# 状态变量 Args:
device_id: 设备ID
config: 配置字典包含max_volume, port等参数
**kwargs: 其他参数,确保兼容性
"""
self.device_id = device_id or "virtual_transfer_pump"
# 从config或kwargs中获取参数确保类型正确
if config:
self.max_volume = float(config.get('max_volume', 25.0))
self.port = config.get('port', 'VIRTUAL')
else:
self.max_volume = float(kwargs.get('max_volume', 25.0))
self.port = kwargs.get('port', 'VIRTUAL')
self._transfer_rate = float(kwargs.get('transfer_rate', 0))
self.mode = kwargs.get('mode', VirtualPumpMode.Normal)
# 状态变量 - 确保都是正确类型
self._status = "Idle" self._status = "Idle"
self._position = 0.0 # 当前柱塞位置 (ml) self._position = 0.0 # float
self._max_velocity = 5.0 # 默认最大速度 (ml/s) self._max_velocity = 5.0 # float
self._current_volume = 0.0 # 当前注射器中的体积 self._current_volume = 0.0 # float
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}") self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""初始化虚拟泵""" """初始化虚拟泵"""
self.logger.info(f"Initializing virtual pump {self.device_id}") self.logger.info(f"Initializing virtual pump {self.device_id}")
@@ -86,30 +102,73 @@ class VirtualPump:
velocity = self._max_velocity velocity = self._max_velocity
return abs(volume) / velocity return abs(volume) / velocity
# 基本泵操 # 新的set_position方法 - 专门用于SetPumpPosition动
async def set_position(self, position: float, velocity: float = None): async def set_position(self, position: float, max_velocity: float = None):
""" """
移动到绝对位置 移动到绝对位置 - 专门用于SetPumpPosition动作
Args: Args:
position (float): 目标位置 (ml) position (float): 目标位置 (ml)
velocity (float): 移动速度 (ml/s) max_velocity (float): 移动速度 (ml/s)
Returns:
dict: 符合SetPumpPosition.action定义的结果
""" """
position = max(0, min(self.max_volume, position)) # 限制在有效范围内 try:
# 验证并转换参数
target_position = float(position)
velocity = float(max_velocity) if max_velocity is not None else self._max_velocity
# 限制位置在有效范围内
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")
# 模拟移动过程
start_position = self._position
steps = 10 if duration > 0.1 else 1 # 如果移动距离很小只用1步
step_duration = duration / steps if steps > 1 else duration
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
# 更新状态
self._status = "Moving" if i < steps else "Idle"
self._position = current_pos
self._current_volume = current_pos
# 等待一小步时间
if i < steps and step_duration > 0:
await asyncio.sleep(step_duration)
volume_to_move = abs(position - self._position) # 确保最终位置准确
duration = self._calculate_duration(volume_to_move, velocity) self._position = target_position
self._current_volume = target_position
self.logger.info(f"Moving to position {position} ml (current: {self._position} ml)") self._status = "Idle"
# 模拟移动过程 self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml")
await self._simulate_operation(duration)
# 返回符合action定义的结果
self._position = position return {
self._current_volume = position # 假设位置等于体积 "success": True,
"message": f"Successfully moved to position {self._position} ml"
self.logger.info(f"Reached position {self._position} ml") }
except Exception as e:
error_msg = f"Failed to set position: {str(e)}"
self.logger.error(error_msg)
return {
"success": False,
"message": error_msg
}
# 其他泵操作方法
async def pull_plunger(self, volume: float, velocity: float = None): async def pull_plunger(self, volume: float, velocity: float = None):
""" """
拉取柱塞(吸液) 拉取柱塞(吸液)
@@ -135,7 +194,7 @@ class VirtualPump:
self._current_volume = 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"Pulled {actual_volume} ml, current volume: {self._current_volume} ml")
async def push_plunger(self, volume: float, velocity: float = None): async def push_plunger(self, volume: float, velocity: float = None):
""" """
推出柱塞(排液) 推出柱塞(排液)
@@ -161,37 +220,18 @@ class VirtualPump:
self._current_volume = 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"Pushed {actual_volume} ml, current volume: {self._current_volume} ml")
# 便捷操作方法 # 便捷操作方法
async def aspirate(self, volume: float, velocity: float = None): async def aspirate(self, volume: float, velocity: float = None):
""" """吸液操作"""
吸液操作
Args:
volume (float): 吸液体积 (ml)
velocity (float): 吸液速度 (ml/s)
"""
await self.pull_plunger(volume, velocity) await self.pull_plunger(volume, velocity)
async def dispense(self, volume: float, velocity: float = None): async def dispense(self, volume: float, velocity: float = None):
""" """排液操作"""
排液操作
Args:
volume (float): 排液体积 (ml)
velocity (float): 排液速度 (ml/s)
"""
await self.push_plunger(volume, velocity) await self.push_plunger(volume, velocity)
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None): async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
""" """转移操作(先吸后排)"""
转移操作(先吸后排)
Args:
volume (float): 转移体积 (ml)
aspirate_velocity (float): 吸液速度 (ml/s)
dispense_velocity (float): 排液速度 (ml/s)
"""
# 吸液 # 吸液
await self.aspirate(volume, aspirate_velocity) await self.aspirate(volume, aspirate_velocity)
@@ -252,7 +292,7 @@ class VirtualPump:
} }
def __str__(self): def __str__(self):
return f"VirtualPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})" return f"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
@@ -261,30 +301,28 @@ class VirtualPump:
# 使用示例 # 使用示例
async def demo(): async def demo():
"""虚拟泵使用示例""" """虚拟泵使用示例"""
pump = VirtualPump("demo_pump", max_volume=50.0) pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0})
await pump.initialize() await pump.initialize()
print(f"Initial state: {pump}") print(f"Initial state: {pump}")
# 测试set_position方法
result = await pump.set_position(10.0, max_velocity=2.0)
print(f"Set position result: {result}")
print(f"After setting position to 10ml: {pump}")
# 吸液测试 # 吸液测试
await pump.aspirate(10.0, velocity=2.0) await pump.aspirate(5.0, velocity=2.0)
print(f"After aspirating 10ml: {pump}") print(f"After aspirating 5ml: {pump}")
# 排液测试
await pump.dispense(5.0, velocity=3.0)
print(f"After dispensing 5ml: {pump}")
# 转移测试
await pump.transfer(3.0)
print(f"After transfer 3ml: {pump}")
# 清空测试 # 清空测试
await pump.empty_syringe() result = await pump.set_position(0.0)
print(f"Empty result: {result}")
print(f"After emptying: {pump}") print(f"After emptying: {pump}")
print("\nPump info:", pump.get_pump_info()) print("\nPump info:", pump.get_pump_info())
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(demo()) asyncio.run(demo())

View File

@@ -202,7 +202,14 @@ virtual_multiway_valve:
set_position: set_position:
type: SendCmd type: SendCmd
goal: goal:
command: position command: command
feedback: {}
result:
success: success
set_valve_position:
type: SendCmd
goal:
command: command
feedback: {} feedback: {}
result: result:
success: success success: success
@@ -559,13 +566,14 @@ virtual_transfer_pump:
description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style)
icon: Pump.webp icon: Pump.webp
class: class:
module: unilabos.devices.virtual.virtual_transferpump:VirtualPump module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump
type: python type: python
status_types: status_types:
status: String status: String
current_volume: Float64 current_volume: Float64
max_volume: Float64 max_volume: Float64
transfer_rate: Float64 transfer_rate: Float64
position: Float64
action_value_mappings: action_value_mappings:
transfer: transfer:
type: Transfer type: Transfer
@@ -587,6 +595,18 @@ virtual_transfer_pump:
result: result:
success: success success: success
message: message message: message
set_position:
type: SetPumpPosition # ← 使用新的动作类型
goal:
position: position # ← 直接映射参数名
max_velocity: max_velocity # ← 直接映射参数名
feedback:
status: status
current_position: current_position
progress: progress
result:
success: success
message: message
# 注射器式转移泵节点配置 - 只有一个双向连接口,可吸入和排出液体 # 注射器式转移泵节点配置 - 只有一个双向连接口,可吸入和排出液体
handles: handles:
- handler_key: transferpump - handler_key: transferpump
@@ -603,12 +623,15 @@ virtual_transfer_pump:
port: port:
type: string type: string
default: "VIRTUAL" default: "VIRTUAL"
description: "通信端口"
max_volume: max_volume:
type: number type: number
default: 50.0 default: 50.0
description: "最大注射器容量 (mL)"
transfer_rate: transfer_rate:
type: number type: number
default: 5.0 default: 5.0
description: "默认转移速率 (mL/s)"
additionalProperties: false additionalProperties: false
virtual_column: virtual_column:

View File

@@ -46,7 +46,7 @@ set(action_files
"action/StopPurge.action" "action/StopPurge.action"
"action/StopStir.action" "action/StopStir.action"
"action/Transfer.action" "action/Transfer.action"
"action/SetPumpPosition.action"
"action/LiquidHandlerProtocolCreation.action" "action/LiquidHandlerProtocolCreation.action"
"action/LiquidHandlerAspirate.action" "action/LiquidHandlerAspirate.action"
"action/LiquidHandlerDiscardTips.action" "action/LiquidHandlerDiscardTips.action"

View File

@@ -0,0 +1,13 @@
# Goal - 目标参数
float64 position # 目标位置 (ml)
float64 max_velocity # 最大速度 (ml/s)
---
# Result - 结果
string return_info
bool success # 操作是否成功
string message # 操作结果消息
---
# Feedback - 反馈
string status # 当前状态
float64 current_position # 当前位置
float64 progress # 进度百分比 (0-100)