From f6f9244ff1c5ae86613ffee2ce9f1b6778cd39bb Mon Sep 17 00:00:00 2001 From: KCFeng425 <2100011801@stu.pku.edu.cn> Date: Tue, 17 Jun 2025 16:56:49 +0800 Subject: [PATCH] =?UTF-8?q?bump=20version=20to=200.9.7=20=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=B8=80=E4=B8=AA=E6=B5=8B=E8=AF=95PumpTransferProtoc?= =?UTF-8?q?ol=E7=9A=84teststation=EF=BC=8C=E4=BA=B2=E6=B5=8B=E5=8F=AF?= =?UTF-8?q?=E4=BB=A5=E8=BF=90=E8=A1=8C=EF=BC=8C=E5=B0=86=E5=85=AB=E9=80=9A?= =?UTF-8?q?=E9=98=80=E4=BB=AC=E5=92=8C=E8=BD=AC=E7=A7=BB=E6=B3=B5=E4=B8=8E?= =?UTF-8?q?pump=5Fprotocol=E9=80=82=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- README_zh.md | 2 +- recipes/ros-humble-unilabos-msgs/recipe.yaml | 2 +- recipes/unilabos/recipe.yaml | 2 +- setup.py | 2 +- .../pumptransferteststation.json | 304 ++++++++++++++++++ .../devices/virtual/virtual_multiway_valve.py | 10 + .../devices/virtual/virtual_transferpump.py | 174 ++++++---- unilabos/registry/devices/virtual_device.yaml | 27 +- unilabos_msgs/CMakeLists.txt | 2 +- unilabos_msgs/action/SetPumpPosition.action | 13 + 11 files changed, 464 insertions(+), 76 deletions(-) create mode 100644 test/experiments/mock_protocol/pumptransferteststation.json create mode 100644 unilabos_msgs/action/SetPumpPosition.action diff --git a/README.md b/README.md index 7f34ad1..94010e9 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name # Currently, you need to install the `unilabos_msgs` package # 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 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/README_zh.md b/README_zh.md index fd772cc..d4f77f4 100644 --- a/README_zh.md +++ b/README_zh.md @@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 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等前置 git clone https://github.com/PyLabRobot/pylabrobot plr_repo diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index 82eb7bd..e476d1b 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.9.6 + version: 0.9.7 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index d809f27..2a48b04 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.9.6" + version: "0.9.7" source: path: ../.. diff --git a/setup.py b/setup.py index 3f71dff..091c2e1 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.9.6', + version='0.9.7', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/test/experiments/mock_protocol/pumptransferteststation.json b/test/experiments/mock_protocol/pumptransferteststation.json new file mode 100644 index 0000000..ab23d8d --- /dev/null +++ b/test/experiments/mock_protocol/pumptransferteststation.json @@ -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" + } + } + ] +} \ No newline at end of file diff --git a/unilabos/devices/virtual/virtual_multiway_valve.py b/unilabos/devices/virtual/virtual_multiway_valve.py index bbdc857..c24b7b1 100644 --- a/unilabos/devices/virtual/virtual_multiway_valve.py +++ b/unilabos/devices/virtual/virtual_multiway_valve.py @@ -192,6 +192,16 @@ class VirtualMultiwayValve: def __str__(self): 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__": diff --git a/unilabos/devices/virtual/virtual_transferpump.py b/unilabos/devices/virtual/virtual_transferpump.py index a680833..a2cba9c 100644 --- a/unilabos/devices/virtual/virtual_transferpump.py +++ b/unilabos/devices/virtual/virtual_transferpump.py @@ -11,23 +11,39 @@ class VirtualPumpMode(Enum): 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): - self.device_id = device_id or "virtual_pump" - self.max_volume = max_volume - self._transfer_rate = transfer_rate - self.mode = mode + def __init__(self, device_id: str = None, config: dict = None, **kwargs): + """ + 初始化虚拟转移泵 - # 状态变量 + 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._position = 0.0 # 当前柱塞位置 (ml) - self._max_velocity = 5.0 # 默认最大速度 (ml/s) - self._current_volume = 0.0 # 当前注射器中的体积 + self._position = 0.0 # float + self._max_velocity = 5.0 # float + 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: """初始化虚拟泵""" self.logger.info(f"Initializing virtual pump {self.device_id}") @@ -86,30 +102,73 @@ class VirtualPump: velocity = self._max_velocity return abs(volume) / velocity - # 基本泵操作 - async def set_position(self, position: float, velocity: float = None): + # 新的set_position方法 - 专门用于SetPumpPosition动作 + async def set_position(self, position: float, max_velocity: float = None): """ - 移动到绝对位置 + 移动到绝对位置 - 专门用于SetPumpPosition动作 Args: 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.logger.info(f"Moving to position {position} ml (current: {self._position} ml)") - - # 模拟移动过程 - await self._simulate_operation(duration) - - self._position = position - self._current_volume = position # 假设位置等于体积 - - self.logger.info(f"Reached position {self._position} 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") + + # 返回符合action定义的结果 + return { + "success": True, + "message": f"Successfully moved to 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): """ 拉取柱塞(吸液) @@ -135,7 +194,7 @@ class VirtualPump: self._current_volume = new_position self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml") - + async def push_plunger(self, volume: float, velocity: float = None): """ 推出柱塞(排液) @@ -161,37 +220,18 @@ class VirtualPump: self._current_volume = new_position self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml") - + # 便捷操作方法 async def aspirate(self, volume: float, velocity: float = None): - """ - 吸液操作 - - Args: - volume (float): 吸液体积 (ml) - velocity (float): 吸液速度 (ml/s) - """ + """吸液操作""" await self.pull_plunger(volume, velocity) async def dispense(self, volume: float, velocity: float = None): - """ - 排液操作 - - Args: - volume (float): 排液体积 (ml) - velocity (float): 排液速度 (ml/s) - """ + """排液操作""" await self.push_plunger(volume, velocity) 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) @@ -252,7 +292,7 @@ class VirtualPump: } 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): return self.__str__() @@ -261,30 +301,28 @@ class VirtualPump: # 使用示例 async def demo(): """虚拟泵使用示例""" - pump = VirtualPump("demo_pump", max_volume=50.0) + pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0}) await pump.initialize() 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) - print(f"After aspirating 10ml: {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.aspirate(5.0, velocity=2.0) + print(f"After aspirating 5ml: {pump}") # 清空测试 - await pump.empty_syringe() + result = await pump.set_position(0.0) + print(f"Empty result: {result}") print(f"After emptying: {pump}") print("\nPump info:", pump.get_pump_info()) if __name__ == "__main__": - asyncio.run(demo()) \ No newline at end of file + asyncio.run(demo()) diff --git a/unilabos/registry/devices/virtual_device.yaml b/unilabos/registry/devices/virtual_device.yaml index af00af1..95816f9 100644 --- a/unilabos/registry/devices/virtual_device.yaml +++ b/unilabos/registry/devices/virtual_device.yaml @@ -202,7 +202,14 @@ virtual_multiway_valve: set_position: type: SendCmd goal: - command: position + command: command + feedback: {} + result: + success: success + set_valve_position: + type: SendCmd + goal: + command: command feedback: {} result: success: success @@ -559,13 +566,14 @@ virtual_transfer_pump: description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style) icon: Pump.webp class: - module: unilabos.devices.virtual.virtual_transferpump:VirtualPump + module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump type: python status_types: status: String current_volume: Float64 max_volume: Float64 transfer_rate: Float64 + position: Float64 action_value_mappings: transfer: type: Transfer @@ -587,6 +595,18 @@ virtual_transfer_pump: result: success: success 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: - handler_key: transferpump @@ -603,12 +623,15 @@ virtual_transfer_pump: port: type: string default: "VIRTUAL" + description: "通信端口" max_volume: type: number default: 50.0 + description: "最大注射器容量 (mL)" transfer_rate: type: number default: 5.0 + description: "默认转移速率 (mL/s)" additionalProperties: false virtual_column: diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 56c8993..db64116 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -46,7 +46,7 @@ set(action_files "action/StopPurge.action" "action/StopStir.action" "action/Transfer.action" - + "action/SetPumpPosition.action" "action/LiquidHandlerProtocolCreation.action" "action/LiquidHandlerAspirate.action" "action/LiquidHandlerDiscardTips.action" diff --git a/unilabos_msgs/action/SetPumpPosition.action b/unilabos_msgs/action/SetPumpPosition.action new file mode 100644 index 0000000..5bbc116 --- /dev/null +++ b/unilabos_msgs/action/SetPumpPosition.action @@ -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) \ No newline at end of file